-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Added #or to ActiveRecord::Relation #9052
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,22 @@ def not(opts, *rest) | |
end | ||
end | ||
|
||
# OrChain objects act as placeholder for queries in which #or does not have any parameter. | ||
# In this case, #or must be chained with any other relation method to return a new relation. | ||
# It is intended to allow .or.where() and .or.named_scope. | ||
class OrChain | ||
def initialize(scope) | ||
@scope = scope | ||
end | ||
|
||
def method_missing(method, *args, &block) | ||
right_relation = @scope.klass.unscoped do | ||
@scope.klass.send(method, *args, &block) | ||
end | ||
@scope.or(right_relation) | ||
end | ||
end | ||
|
||
Relation::MULTI_VALUE_METHODS.each do |name| | ||
class_eval <<-CODE, __FILE__, __LINE__ + 1 | ||
def #{name}_values # def select_values | ||
|
@@ -452,6 +468,61 @@ def where!(opts = :chain, *rest) # :nodoc: | |
end | ||
end | ||
|
||
# Returns a new relation, which is the result of filtering the current relation | ||
# according to the conditions in the arguments, joining WHERE clauses with OR | ||
# operand, contrary to the default behaviour that uses AND. | ||
# | ||
# #or accepts conditions in one of several formats. In the examples below, the resulting | ||
# SQL is given as an illustration; the actual query generated may be different depending | ||
# on the database adapter. | ||
# | ||
# === without arguments | ||
# | ||
# If #or is used without arguments, it returns an ActiveRecord::OrChain object that can | ||
# be used to chain queries with any other relation method, like where: | ||
# | ||
# Post.where("id = 1").or.where("id = 2") | ||
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) | ||
# | ||
# It can also be chained with a named scope: | ||
# | ||
# Post.where("id = 1").or.containing_the_letter_a | ||
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'body LIKE \\'%a%\\'')) | ||
# | ||
# === ActiveRecord::Relation | ||
# | ||
# When #or is used with an ActiveRecord::Relation as an argument, it merges the two | ||
# relations, with the exception of the WHERE clauses, that are joined using the OR | ||
# operand. | ||
# | ||
# Post.where("id = 1").or(Post.where("id = 2")) | ||
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) | ||
# | ||
# === anything you would pass to #where | ||
# | ||
# #or also accepts anything that could be passed to the #where method, as | ||
# a shortcut: | ||
# | ||
# Post.where("id = 1").or("id = ?", 2) | ||
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) | ||
# | ||
def or(opts = :chain, *rest) | ||
if opts == :chain | ||
OrChain.new(self) | ||
else | ||
left = with_default_scope | ||
right = (ActiveRecord::Relation === opts) ? opts : klass.unscoped.where(opts, rest) | ||
|
||
unless left.where_values.empty? || right.where_values.empty? | ||
left, right = left.spawn, right.spawn | ||
left.where_values = [left.where_ast.or(right.where_ast)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a problem here: you are modifying the original relations. If you're lucky, this will fail with The fix is simple:
|
||
right.where_values = [] | ||
end | ||
|
||
left.merge!(right) | ||
end | ||
end | ||
|
||
# Allows to specify a HAVING clause. Note that you can't use HAVING | ||
# without also specifying a GROUP clause. | ||
# | ||
|
@@ -727,6 +798,23 @@ def build_arel | |
arel | ||
end | ||
|
||
# Returns an Arel AST containing only where_values | ||
def where_ast | ||
arel_wheres = [] | ||
|
||
where_values.each do |where| | ||
arel_wheres << (String === where ? Arel.sql(where) : where) | ||
end | ||
|
||
return Arel::Nodes::And.new(arel_wheres) if arel_wheres.length >= 2 | ||
|
||
if Arel::Nodes::SqlLiteral === arel_wheres.first | ||
Arel::Nodes::Grouping.new(arel_wheres.first) | ||
else | ||
arel_wheres.first | ||
end | ||
end | ||
|
||
private | ||
|
||
def custom_join_ast(table, joins) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
require "cases/helper" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to be consistent with other code I think better to use: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i love your attention to detail but that's extreme :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see as extreme. I'm not saying that you need to change but this make totally sense to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i only see it as extreme because it doesn't really have any practical implications. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Single or double quotes are fine, but like I said don't need to change.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ty |
||
require 'models/post' | ||
|
||
module ActiveRecord | ||
class OrTest < ActiveRecord::TestCase | ||
fixtures :posts | ||
|
||
def test_or_with_relation | ||
expected = Post.where('id = 1 or id = 2').to_a | ||
assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a | ||
end | ||
|
||
def test_or_with_string | ||
expected = Post.where('id = 1 or id = 2').to_a | ||
assert_equal expected, Post.where('id = 1').or('id = 2').to_a | ||
end | ||
|
||
def test_or_chaining | ||
expected = Post.where('id = 1 or id = 2').to_a | ||
assert_equal expected, Post.where('id = 1').or.where('id = 2').to_a | ||
end | ||
|
||
def test_or_without_left_where | ||
expected = Post.where('id = 1').to_a | ||
assert_equal expected, Post.or('id = 1').to_a | ||
end | ||
|
||
def test_or_without_right_where | ||
expected = Post.where('id = 1').to_a | ||
assert_equal expected, Post.where('id = 1').or(Post.all).to_a | ||
end | ||
|
||
def test_or_preserves_other_querying_methods | ||
expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a | ||
assert_equal expected, Post.where('id = 1').order('body asc').or(:id => [2, 3]).to_a | ||
end | ||
|
||
def test_or_with_named_scope | ||
expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a | ||
assert_equal expected, Post.where('id = 1').or.containing_the_letter_a | ||
end | ||
|
||
def test_or_on_loaded_relation | ||
expected = Post.where('id = 1 or id = 2').to_a | ||
p = Post.where('id = 1') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
p.load | ||
assert_equal p.loaded?, true | ||
assert_equal expected, p.or('id = 2').to_a | ||
end | ||
|
||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move changelog entry to the first line