diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index ceded67d57b2d..a7c894ea22f10 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -13,7 +13,7 @@ module Querying :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by, :find_each, :find_in_batches, :in_batches, :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins, - :where, :rewhere, :invert_where, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, + :where, :wharel, :rewhere, :invert_where, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :and, :or, :annotate, :optimizer_hints, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only, :count, :average, :minimum, :maximum, :sum, :calculate, diff --git a/activerecord/lib/active_record/relation/query_composer.rb b/activerecord/lib/active_record/relation/query_composer.rb new file mode 100644 index 0000000000000..728ab70295e28 --- /dev/null +++ b/activerecord/lib/active_record/relation/query_composer.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module ActiveRecord + class QueryComposer + def initialize(scope) + @scope = scope + @arel_table = scope.arel_table + @reflections = scope._reflections + @attribute_nodes = {} + define_attribute_accessors + end + + def method_missing(name, *_args) + if reflections.key?(name.to_s) + self.class.new(reflections[name.to_s].klass) + else + super + end + end + + private + attr_reader :scope, :arel_table, :reflections + + def define_attribute_accessors + scope.attribute_names.each do |attr| + define_singleton_method attr do + @attribute_nodes[attr] ||= Node.new(arel_table[attr]) + end + end + end + + class Node + MAPPED_METHODS = { + eq: :==, + not_eq: :!=, + gt: :>, + gteq: :>=, + lt: :<, + lteq: :<=, + in: :in, + and: :and, + or: :or, + matches: :like, + does_not_match: :not_like + } + + def initialize(arel_node) + @arel_node = arel_node + end + + MAPPED_METHODS.each do |arel_method, exposed_method| + define_method exposed_method do |other| + other = other.arel_node if self.class == other.class + Node.new(arel_node.public_send(arel_method, other)) + end + end + + def to_arel + arel_node + end + + protected + attr_reader :arel_node + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index c9da47482493a..b888b55f8af06 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -2,6 +2,7 @@ require "active_record/relation/from_clause" require "active_record/relation/query_attribute" +require "active_record/relation/query_composer" require "active_record/relation/where_clause" require "active_model/forbidden_attributes_protection" require "active_support/core_ext/array/wrap" @@ -103,6 +104,11 @@ def missing(*associations) @scope end + + def satisfies(&block) + conditions = yield QueryComposer.new(@scope) + @scope.where(conditions.to_arel) + end end FROZEN_EMPTY_ARRAY = [].freeze diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 23599ba4cc9fb..8d277f1500f61 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -153,5 +153,33 @@ def test_rewhere_with_infinite_range assert_equal expected.to_a, relation.to_a end + + def test_where_satisfies + relation = Post.where.satisfies { |post| (post.author_id != 1).and(post.id > 2) } + expected = Post.where("posts.author_id != ? AND posts.id > ?", 1, 2) + + assert_equal expected.to_a, relation.to_a + end + + def test_where_satisfies_with_like + relation = Post.where.satisfies { |post| post.title.like("%comments%") } + expected = Post.where("posts.title LIKE ?", "%comments%") + + assert_equal expected.to_a, relation.to_a + end + + def test_where_satisfies_with_not_like + relation = Post.where.satisfies { |post| post.title.not_like("%comments%") } + expected = Post.where("posts.title NOT LIKE ?", "%comments%") + + assert_equal expected.to_a, relation.to_a + end + + def test_where_satisfies_on_joined_table + relation = Post.joins(:comments).where.satisfies { |post| post.comments.type.like("%Special%") }.to_a + expected = Post.joins(:comments).where("comments.type LIKE ?", "%Special%").to_a + + assert_equal expected.to_a, relation.to_a + end end end