diff options
Diffstat (limited to 'activerecord')
28 files changed, 287 insertions, 123 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a4cf6023e1..70549243a8 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,46 @@ +* Fix a bug where using `t.foreign_key` twice with the same `to_table` within + the same table definition would only create one foreign key. + + *George Millo* + +* Fix a regression on has many association, where calling a child from parent in child's callback + results in same child records getting added repeatedly to target. + + Fixes #13387. + + *Bogdan Gusiev*, *Jon Hinson* + +* Rework `ActiveRecord::Relation#last` + + 1. Never perform additional SQL on loaded relation + 2. Use SQL reverse order instead of loading relation if relation doesn't have limit + 3. Deprecated relation loading when SQL order can not be automatically reversed + + Topic.order("title").load.last(3) + # before: SELECT ... + # after: No SQL + + Topic.order("title").last + # before: SELECT * FROM `topics` + # after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1 + + Topic.order("coalesce(author, title)").last + # before: SELECT * FROM `topics` + # after: Deprecation Warning for irreversible order + + *Bogdan Gusiev* + + +* Allow `joins` to be unscoped. + + Closes #13775. + + *Takashi Kokubun* + +* Add ActiveRecord `#second_to_last` and `#third_to_last` methods. + + *Brian Christian* + * Added `numeric` helper into migrations. Example: @@ -612,7 +655,7 @@ *Ben Murphy*, *Matthew Draper* -* `bin/rake db:migrate` uses +* `bin/rails db:migrate` uses `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of `Migrator.migrations_paths`. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 9f2c7292ea..2dca6b612e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -136,6 +136,14 @@ module ActiveRecord first_nth_or_last(:forty_two, *args) end + def third_to_last(*args) + first_nth_or_last(:third_to_last, *args) + end + + def second_to_last(*args) + first_nth_or_last(:second_to_last, *args) + end + def last(*args) first_nth_or_last(:last, *args) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fe693cfbb6..2a9627a474 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -197,6 +197,16 @@ module ActiveRecord @association.forty_two(*args) end + # Same as #first except returns only the third-to-last record. + def third_to_last(*args) + @association.third_to_last(*args) + end + + # Same as #first except returns only the second-to-last record. + def second_to_last(*args) + @association.second_to_last(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. 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 708b3af5bd..c5fbe0d1d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -54,12 +54,18 @@ module ActiveRecord end scope_chain_index += 1 - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact + klass_scope = + if klass.current_scope + klass.current_scope.clone + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 423a93964e..e902eb7531 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -34,30 +34,6 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class AttributeMethodCache - def initialize - @module = Module.new - @method_cache = Concurrent::Map.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*'.freeze).first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @module.instance_method temp_method - end - end - - private - - # Override this method in the subclasses for method body. - def method_body(method_name, const_name) - raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." - end - end - class GeneratedAttributeMethods < Module; end # :nodoc: module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 5197e21fa4..ab2ecaa7c5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,8 +1,11 @@ module ActiveRecord module AttributeMethods module Read - ReaderMethodCache = Class.new(AttributeMethodCache) { - private + extend ActiveSupport::Concern + + module ClassMethods + protected + # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. # Evaluating many similar methods may use more memory as the instruction @@ -21,21 +24,6 @@ module ActiveRecord # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - EOMETHOD - end - }.new - - extend ActiveSupport::Concern - - module ClassMethods - protected - def define_method_attribute(name) safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bbf2a51a0e..5599b590ca 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,19 +1,6 @@ module ActiveRecord module AttributeMethods module Write - WriterMethodCache = Class.new(AttributeMethodCache) { - private - - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name}(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - write_attribute(name, value) - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern included do diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 854f9776a3..1f1b11eb68 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -179,7 +179,7 @@ module ActiveRecord # # If the +before_validation+ callback throws +:abort+, the process will be # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. - # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception. # Nothing will be appended to the errors object. # # == Canceling callbacks 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 7e0c9f7837..bb5119d64e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -27,10 +27,10 @@ module ActiveRecord end # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - if arel.is_a?(String) + if arel.is_a?(String) && preparable.nil? preparable = false else preparable = visitor.preparable diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 5e27cfe507..33dbab41cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -61,11 +61,11 @@ module ActiveRecord @query_cache.clear end - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds) } + cache_sql(sql, binds) { super(sql, name, binds, preparable: visitor.preparable) } else super end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index cb10ca9929..4f97c7c065 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -212,7 +212,7 @@ module ActiveRecord def initialize(name, temporary, options, as = nil) @columns_hash = {} @indexes = {} - @foreign_keys = {} + @foreign_keys = [] @primary_keys = nil @temporary = temporary @options = options @@ -330,7 +330,7 @@ module ActiveRecord end def foreign_key(table_name, options = {}) # :nodoc: - foreign_keys[table_name] = options + foreign_keys.push([table_name, options]) end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 1f429cfd94..de5b42e987 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,7 +1,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all - delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all @@ -35,8 +35,8 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] - def find_by_sql(sql, binds = []) - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + def find_by_sql(sql, binds = [], preparable: nil) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 99c0e71f97..956fe7c51e 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -239,6 +239,10 @@ module ActiveRecord def alias_candidate(name) "#{plural_name}_#{name}" end + + def chain + collect_join_chain + end end # Base class for AggregateReflection and AssociationReflection. Objects of @@ -421,7 +425,7 @@ module ActiveRecord # A chain of reflections from this one back to the owner. For more see the explanation in # ThroughReflection. - def chain + def collect_join_chain [self] end @@ -495,6 +499,18 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + def add_as_source(seed) + seed + end + + def add_as_polymorphic_through(reflection, seed) + seed + [PolymorphicReflection.new(self, reflection)] + end + + def add_as_through(seed) + seed + [self] + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -742,19 +758,8 @@ module ActiveRecord # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # - def chain - @chain ||= begin - a = source_reflection.chain - b = through_reflection.chain.map(&:dup) - - if options[:source_type] - b[0] = PolymorphicReflection.new(b[0], self) - end - - chain = a + b - chain[0] = self # Use self so we don't lose the information from :source_type - chain - end + def collect_join_chain + collect_join_reflections [self] end # This is for clearing cache on the reflection. Useful for tests that need to compare @@ -913,6 +918,27 @@ module ActiveRecord scope_chain end + def add_as_source(seed) + collect_join_reflections seed + end + + def add_as_polymorphic_through(reflection, seed) + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) + end + + def add_as_through(seed) + collect_join_reflections(seed + [self]) + end + + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end + end + protected def actual_source_reflection # FIXME: this is a horrible name diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index d48bcea28a..16563515f6 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -145,15 +145,21 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_attribute(primary_key).desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first + rescue ActiveRecord::IrreversibleOrderError + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Finding a last element by loading the relation when SQL ORDER + can not be reversed is deprecated. + Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. + Please call `to_a.last` if you still want to load the relation. + WARNING + find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record @@ -242,6 +248,38 @@ module ActiveRecord find_nth! 41 end + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth(-3) + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + find_nth!(-3) + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth(-2) + end + + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + find_nth!(-2) + end + # Returns true if a record exists in the table that matches the +id+ or # conditions given, or false otherwise. The argument can take six forms: # @@ -298,7 +336,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a ActiveRecord::RecordNotFound exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number @@ -523,19 +561,6 @@ module ActiveRecord relation.limit(limit).to_a end - def find_last - if loaded? - @records.last - else - @last ||= - if limit_value - to_a.last - else - reverse_order.limit(1).to_a.first - end - end - end - private def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: @@ -546,5 +571,9 @@ module ActiveRecord find_nth_with_limit(index, limit) end end + + def find_last(limit) + limit ? to_a.last(limit) : to_a.last + end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 91bfa4d131..4533f3263f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -655,6 +655,10 @@ module ActiveRecord # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) + unless other.is_a? Relation + raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." + end + spawn.or!(other) end @@ -1112,6 +1116,8 @@ module ActiveRecord order_query.flat_map do |o| case o + when Arel::Attribute + o.desc when Arel::Nodes::Ordering o.reverse when String diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2bfc5ff7ae..a9e1fd0dad 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -60,7 +60,7 @@ module ActiveRecord end # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a ORDER clause. + # them into a valid SQL fragment for an ORDER clause. # # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) # # => "field(id, 1,3,2)" diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index f6b0efb88a..6c896ccea6 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -106,7 +106,7 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection - klass.find_by_sql sql, bind_values + klass.find_by_sql(sql, bind_values, preparable: true) end alias :call :execute end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6677e6dc5f..ecaf04e39e 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -45,7 +45,7 @@ module ActiveRecord end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but - # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. + # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise_validation_error end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a376e2a17f..f0aa4521b5 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -19,7 +19,7 @@ module ActiveRecord relation = build_relation(finder_class, table, attribute, value) if record.persisted? && finder_class.primary_key.to_s != attribute.to_s if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id) + relation = relation.where.not(finder_class.primary_key => record.id_was) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index ecaa521283..e975f4fbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -408,6 +408,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.third_to_last() + bulbs.third_to_last({}) + end + + assert_no_queries do + bulbs.second_to_last() + bulbs.second_to_last({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 3602ee7ba2..84aac3e721 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -108,7 +108,7 @@ class EachTest < ActiveRecord::TestCase end end - def test_find_in_batches_should_finish_the_end_option + def test_find_in_batches_should_end_at_the_finish_option assert_queries(6) do Post.find_in_batches(batch_size: 1, finish: 5) do |batch| assert_kind_of Array, batch @@ -316,7 +316,7 @@ class EachTest < ActiveRecord::TestCase end end - def test_in_batches_should_finish_the_end_option + def test_in_batches_should_end_at_the_finish_option post = Post.order('id DESC').where('id <= ?', 5).first assert_queries(7) do relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 75a74c052d..5b41630cd8 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -101,7 +101,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_where_and_limit # Please note that Topic 1 is the only not approved so - # if it were among the first 3 it would raise a ActiveRecord::RecordNotFound + # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) assert_equal 3, records.size assert_equal 'The Third Topic of the day', records[0].title @@ -516,16 +516,44 @@ class FinderTest < ActiveRecord::TestCase assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) end - def test_last_with_integer_and_order_should_not_use_sql_limit - query = assert_sql { Topic.order("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_order_should_use_sql_limit + relation = Topic.order("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? end - def test_last_with_integer_and_reorder_should_not_use_sql_limit - query = assert_sql { Topic.reorder("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_reorder_should_use_sql_limit + relation = Topic.reorder("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_on_loaded_relation_should_not_use_sql + relation = Topic.limit(10).load + assert_no_queries do + relation.last + relation.last(2) + end + end + + def test_last_with_irreversible_order + assert_deprecated do + Topic.order("coalesce(author_name, title)").last + end + end + + def test_last_on_relation_with_limit_and_offset + post = posts('sti_comments') + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end def test_take_and_first_and_last_with_integer_should_return_an_array diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index b01415afb2..85435f4dbc 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -144,6 +144,22 @@ module ActiveRecord @connection.drop_table "testing", if_exists: true end end + + test "multiple foreign keys can be added to the same table" do + @connection.create_table :testings do |t| + t.integer :col_1 + t.integer :col_2 + + t.foreign_key :testing_parents, column: :col_1 + t.foreign_key :testing_parents, column: :col_2 + end + + fks = @connection.foreign_keys("testings") + + fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "col_1"], + ["testings", "testing_parents", "col_2"]], fk_definitions) + end end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 28a0862f91..ce8c5ca489 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -82,5 +82,11 @@ module ActiveRecord assert_equal p.loaded?, true assert_equal expected, p.or(Post.where('id = 2')).to_a end + + def test_or_with_non_relation_object_raises_error + assert_raises ArgumentError do + Post.where(id: [1, 2, 3]).or(title: 'Rails') + 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 ad5ca70f36..c918cbdef5 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -374,6 +374,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end + def test_default_scope_with_joins + assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count, + Comment.joins(:special_post_with_default_scope).count + assert_equal Comment.where(post_id: Post.pluck(:id)).count, + Comment.joins(:post).count + end + + def test_unscoped_with_joins_should_not_have_default_scope + assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, + Comment.joins(:post).to_a + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index ab63f5825c..bce86875e1 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -177,6 +177,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:color], first_model.stored_attributes[:data] assert_equal [:color, :width, :height], second_model.stored_attributes[:data] assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + assert_equal [:color], first_model.stored_attributes[:data] end test "YAML coder initializes the store when a Nil value is given" do diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 7502a55391..e601c53dbf 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -469,4 +469,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert_match(/\AUnknown primary key for table dashboards in model/, e.message) assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) end + + def test_validate_uniqueness_ignores_itself_when_primary_key_changed + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "This is a unique title") + assert t.save, "Should save t as unique" + + t.id += 1 + assert t.valid?, "Should be valid" + assert t.save, "Should still save t as unique" + end end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index b38b17e90e..dcc5c5a310 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -14,6 +14,7 @@ class Comment < ActiveRecord::Base has_many :ratings belongs_to :first_post, :foreign_key => :post_id + belongs_to :special_post_with_default_scope, foreign_key: :post_id has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count |