Skip to content

Where satisfies: constrained arel, block with method, use where chain #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/querying.rb
Original file line number Diff line number Diff line change
@@ -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,
66 changes: 66 additions & 0 deletions activerecord/lib/active_record/relation/query_composer.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions activerecord/lib/active_record/relation/query_methods.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions activerecord/test/cases/relation/where_chain_test.rb
Original file line number Diff line number Diff line change
@@ -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) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the wharel method, the satisfies after the where seems confusing (at least to me). Do Rails provide another API similar to this one?

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