diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6e13bce54258a..e0ae2b98adfb5 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,11 @@ +* Add `ActiveRecord::Relation#structurally_compatible?`. + + Adds a query method by which a user can tell if the relation that they're + about to use for `#or` or `#and` is structurally compatible with the + receiver. + + *Kevin Newton* + * Add `ActiveRecord::QueryMethods#in_order_of`. This allows you to specify an explicit order that you'd like records diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2076796ead819..ee6d366c90712 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -748,6 +748,21 @@ def invert_where! # :nodoc: self end + # Checks whether the given relation is structurally compatible with this relation, to determine + # if it's possible to use the #and and #or methods without raising an error. Structurally + # compatible is defined as: they must be scoping the same model, and they must differ only by + # #where (if no #group has been defined) or #having (if a #group is present). + # + # Post.where("id = 1").structurally_compatible?(Post.where("author_id = 3")) + # # => true + # + # Post.joins(:comments).structurally_compatible?(Post.where("id = 1")) + # # => false + # + def structurally_compatible?(other) + structurally_incompatible_values_for(other).empty? + end + # Returns a new relation, which is the logical intersection of this relation and the one passed # as an argument. # diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 79c5e45f48f00..d55e2ab8efd87 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -50,7 +50,7 @@ class QueryingMethodsDelegationTest < ActiveRecord::TestCase ActiveRecord::FinderMethods.public_instance_methods(false) - [:include?, :member?, :raise_record_not_found_exception!] + ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] + ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method| - method.end_with?("=", "!", "value", "values", "clause") + method.end_with?("=", "!", "?", "value", "values", "clause") } - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [ :any?, :many?, :none?, :one?, :first_or_create, :first_or_create!, :first_or_initialize, diff --git a/activerecord/test/cases/relation/structural_compatibility_test.rb b/activerecord/test/cases/relation/structural_compatibility_test.rb new file mode 100644 index 0000000000000..0c79b29254de2 --- /dev/null +++ b/activerecord/test/cases/relation/structural_compatibility_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +module ActiveRecord + class StructuralCompatibilityTest < ActiveRecord::TestCase + fixtures :posts + + def test_compatible_values + left = Post.where(id: 1) + right = Post.where(id: 2) + + assert left.structurally_compatible?(right) + end + + def test_incompatible_single_value_relations + left = Post.distinct.where("id = 1") + right = Post.where(id: [2, 3]) + + assert_not left.structurally_compatible?(right) + end + + def test_incompatible_multi_value_relations + left = Post.order("body asc").where("id = 1") + right = Post.order("id desc").where(id: [2, 3]) + + assert_not left.structurally_compatible?(right) + end + + def test_incompatible_unscope + left = Post.order("body asc").where("id = 1").unscope(:order) + right = Post.order("body asc").where("id = 2") + + assert_not left.structurally_compatible?(right) + end + end +end