Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added #or to ActiveRecord::Relation #9052

Closed
wants to merge 1 commit into from

44 participants

Gael Muller Matt Bridges Rafael Mendonça França housekeeper Greg Molnar Stefano Maxim Zhukov Ilya Katz Kevin Bongart Robin Boutros ghalley Sean Todd Alexandre Testu Julien Capron Christopher Keele Will Lefteris Laskaridis Adrien Coquio Paolo cmititiuc Ilia Lobsanov Sven Schwyn Daniel Ferraz Shireesh Jayashetty and others
Gael Muller

ActiveRecord::Relation#or 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, contraty to the default behaviour that uses AND.

ActiveRecord::Relation#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 ActiveRecord::Relation#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

ActiveRecord::Relation#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'))

This is my first contribution to rails and I tried to keep things coherent with the current code, but I am obviously open for feedback.

activerecord/lib/active_record/relation/query_methods.rb
@@ -452,6 +468,60 @@ 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, contraty to the default behaviour that uses AND.

Typo contrary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Matt Bridges

:+1: Would love to see this one added. Simple idea that has been missing for a long time just like not.

activerecord/lib/active_record/relation/query_methods.rb
@@ -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 {
+ @scope.klass.send(method, *args, &block)
+ }

Generally multi-line blocks use the do..end style.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Gael Muller

Thank you @mattdbridges for your feedback. I updated the commit with corrections for the typo and the multiline block.

Rafael Mendonça França

Sorry but some time ago we reached the conclusion to not add this method.

Some reason:

  1. It opens the door for wanting to expand this line of DSL further (including undesireable ventures into greater_or_equal_than).
  2. It's being a fairly uncommon query not in need of optimization.
  3. OR is usually a query smell

Thank you so much.

Gael Muller

@rafaelfranca, I feel like the arguments to not add this method are those used for the like method that was rollbacked, but I think it doesn't apply here.

  1. This seems like a common need. You only have to look at this issue (#5545), or the several forums/stack overflow posts to realize that.
  2. This actually is not just an optimization, but a missing feature. With like or greater_or_equal_than, you can always easily write a where clause that would do the exact same thing. I don't see any way to join two scopes with a "OR" without rewriting the whole query in sql (which defeats the purpose of ActiveRecord).

If you still disagree, I will try to extract it as a plugin.

Gael Muller

@tenderlove, @steveklabnik, you commented on a similar pull request (#6817). What is your opinion about this ?

Rafael Mendonça França

I don't have strong opinions about this one, so I think we can reopen to discuss.

@jeremy thoughts?

Rafael Mendonça França rafaelfranca reopened this
housekeeper

I would +1 this too but for a true scoping issue...

Developing a re-occurring calendaring gem and it would be awesome to chain scopes in the following ways

Calendar::Event.for_current_user.today
Calendar::Event.for_current_user.this_week
Calendar::Event.for_current_user.next_week

there are 3 scoped chains for 3 tables... an events table based on whether the user created the event or whether the user has been assigned/notified to/of the event in an event_users table. This is handled with no problem, my issue is when there is a re-occurring event which falls between a reoccurring start and end as well as an event start and end of which I need to OR - this is perfectly valid reason for this request and I believe a missing feature especially when wanting to create complex but efficient queries

FYI
An event only has one entry in the table
if this event re-occurs, its reoccurrence type is stored and the start/stop datetimes NOT each reoccurrence as an event.

Greg Molnar

:+1: for this feature

Maxim Zhukov

:+1: for this feature

Ilya Katz

+1

Gael Muller gaelmuller Added #or to ActiveRecord::Relation
ActiveRecord::Relation#or 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, contraty to the default behaviour that uses AND.

ActiveRecord::Relation#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 ActiveRecord::Relation#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

ActiveRecord::Relation#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'))
2366cc5
ghalley

+1

Sean Todd

+1 for this feature. I need some way to merge scopes together without having to do it manually column-by-column in squeel.

Christopher Keele

+1
This would clean up those corners of my code where I feel like I'm not using an ORM anymore.

Will

In my humble opinion, leaving this to arel is a much more elegant

ie

user_arel_table = User.arel_table
User.where(user_arel_table[:name].eq('bob').or(user_arel_table[:age].lt(25))).to_sql

translates to

SELECT "users".* FROM "users"  WHERE (("users"."name" = 'bob' OR "users"."age" < 25))

or to convert one of your examples from the pull request

Post.where("id = 1").or(Post.where("id = 2"))

posts = Post.arel_table
Post.where(posts[:id].eq(1).or(posts[:id].eq(2))).to_sql
SELECT "posts".* FROM "posts"  WHERE (("posts"."id" = 1 OR "posts"."id" = 2))

you can also check out a couple other good examples here (only the upvoted ones of course)

http://stackoverflow.com/questions/7976358/activerecord-arel-or-condition

you can also join in other model/tables and then use their arel tables also. I can add some examples like that if anyone wants to see.

Lefteris Laskaridis

It's embarrassing enough that this feature is NOT in ActiveRecord yet. +1

Sean Todd

@williscool That example works for small queries, but what if I have a large number of columns to search across? When I try the inject methods in the SO example I end up with Hash objects instead of ARel objects which don't work. I would prefer to have a native solution to combining scopes with OR. Rails is really good about having elegant solutions to many things, but this doesn't seem to be one of them.

Will

@descentintomael can you give an example with a large number of columns where the arel version is cumbersome?

And you would do your method chaining in between the where functions on the ActiveRecord::Relation objects. Not in the middle of them on the arel ones.

Man the naming of those things is quite confusing I wish they had completely different names.

What I mean is

User.containing_the_letter_a.where(admins.or(authors))

not

User.where(admins.or(authors).containing_the_letter_a)
Sean Todd

Right now I'm trying to search through my Order model which has quite a few columns. For instance, it has an amount and a currency column, those two need to be joined with 'AND' but then there are other columns like customer ID and PO number that come into play and need to be combined with 'OR'.

I'm not really sure yet how the syntax would go (without using Squeel), but I do like the direction taken in the first example:

Post.where("id = 1").or(Post.where("id = 2"))
activerecord/lib/active_record/relation/query_methods.rb
((35 lines not shown))
+ #
+ # #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.where_values = [left.where_ast.or(right.where_ast)]

There's a problem here: you are modifying the original relations. If you're lucky, this will fail with ActiveRecord::ImmutableRelation, but this only happens if the relations have already been loaded.

The fix is simple:

diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 1e291e6..ced097c 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -607,6 +607,7 @@ module ActiveRecord
         right = (ActiveRecord::Relation === opts) ? opts : klass.unscoped.where(opts, rest)

         unless left.where_values.empty? || right.where_values.empty?
+          left, right = left.dup, right.dup
           left.where_values = [left.where_ast.or(right.where_ast)]
           right.where_values = []
         end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Gael Muller

@schuetzm, thank you for your feedback. I updated the commit to avoid ImmutableRelation exceptions.

Justin McNally

I converted this into something that could be used as a monkey patch since the rails team doesnt seem to want to support this. I really needed it. If anyone else does add the following gist as an initializer.

https://gist.github.com/j-mcnally/250eaaceef234dd8971b

Adrien Coquio

:+1: on this

Deleted user

huge +1. google brought me here.
for something like, 'Event.due_in(5.days..8.days).or.due_in(9.days..11.days)' its super useful.

Paul Nikitochkin pftg commented on the diff
activerecord/test/cases/relation/or_test.rb
((30 lines not shown))
+ 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')
Paul Nikitochkin
pftg added a note

p = Post.where('id = 1').load will use less lines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Paul Nikitochkin pftg commented on the diff
activerecord/test/cases/relation/or_test.rb
@@ -0,0 +1,52 @@
+require "cases/helper"
Paul Nikitochkin
pftg added a note

to be consistent with other code I think better to use: require 'cases/helper'

Deleted user
ghost added a note

i love your attention to detail but that's extreme :)

Rafael Mendonça França Owner

I don't see as extreme. I'm not saying that you need to change but this make totally sense to me.

Deleted user
ghost added a note

i only see it as extreme because it doesn't really have any practical implications.
if i had to apply logic to this I think double quotes because I can easily use string interpolation there too but it seems like a small thing to worry about. i dont know how precise rails contributions need to be.

Rafael Mendonça França Owner

Single or double quotes are fine, but like I said don't need to change.

I Don't know how precise rails contributions need to be.

Deleted user
ghost added a note

ty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Paolo

:+1: I would love to have this feature added to active record relation without having to use arel_tables

Deleted user

I don't see how arel_tables scales to all the use cases that this method can.
for example, imagine "due_in" as something that did a JOIN and condition on another model.
i'd need to recreate that query with AREL, where as I could say due_in(2..3.days).or.due_in(6..7.days).

Deleted user

I also think activerecord is the public interface people should be talking to, not really AREL, right?

Deleted user

a very contrived example:

  def self.due_on(range)
    joins(:users).where(users: {id: 1..10}).where(due_on: range)
  end

I could accept two arguments here, too, perhaps do the OR that way with AREL, but it's hardly optimal and makes the interface for the method weird.

Deleted user

hey rails team
could we get some feedback on this? if it isn't going to be accepted it'd make a fantastic gem.

Paul Nikitochkin pftg commented on the diff
activerecord/CHANGELOG.md
@@ -19,6 +19,18 @@
*Alexander Grebennik*
+* Added the `#or` method on ActiveRecord::Relation, in order to use the OR
Paul Nikitochkin
pftg added a note

Please move changelog entry to the first line

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Rafael Mendonça França

I'm not saying this will be accepted or not but I'm adding it to Rails 4.1 milestone. No promises since this was already rejected before and nothing changed about our concerns.

Deleted user

@rafaelfranca thanks for the clearing that up.
it would be nice to have a yay/nay so the author knows he can go on and provide this as a gem otherwise.

Rafael Mendonça França

It still can create the gem to use in Rails 4. We are using this path even inside the Rails Core. A lot of Rails 4 features was created as gem first.

Deleted user

okay, @gaelmuller still alive? :)

Rafael Mendonça França

Just to be clear again, I don't have strong opinions about this feature since I never needed such feature in my applications, this is why I not merged yet. I'll defer to members of core that have a better opinion than I on this one.

I'm planning to start Rails 4.1 development :soon: so I'll revisit this PR.

Gael Muller

@robgleeson, still alive ! But currently in vacations. I'll update this PR at my return.

cmititiuc

+1 for this feature

in the mean-time, I'm doing this:

http://stackoverflow.com/questions/6686920/activerecord-query-union

(specifically:

find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")

as suggested by Wizard of Ogz)

Now that I think about it more, this might not be the same thing. Still, +1

Ilia Lobsanov

can this be used with associations?

cmititiuc

@nurey I tried it. It doesn't seem to work with associations.

Gael Muller

@nurey @cmititiuc, could you give an example you expected to work but that doesn't ?

Sven Schwyn

+1 for or, -1 for any_of

Shireesh Jayashetty

In my opinion, adding an or also warrants adding an and to keep the DSL symmetric (is that the right word?!).

Currently and is implied when where's are chained. It can be argued that the implied meaning should be or.

I also feel, the verbs or_where and and_where are better than just or and and

Deleted user

I'd vote for retaining an implicit AND in where() and offering or() as a separate proxy to building another OR where().
I think it'd cause the least upset for now. "where(x: y).and.where(f: z)" could be something to add that gracefully degrades to support current implicit AND.

James Sanders

:+1: This is my third or so time visiting this PR and hoping to see it merged!

idrinkpabst

+1 for this.
Use case. Let's say I have a Car and Truck class. But some Cars are Trucklike, so when I want to make a new class Trucklike that returns both Car and Truck objects as an ActiveRecord::Relation so that I can continue to chain scopes.

Dorian Marié

For instance, I have Product.normal, Product.boxes, and Product.gifts, and need to write scopes for each Product.normal_or_boxes, Product.normal_or_gifts, etc...

(In reality I have 5 different kinds of products and don't want to write scope for each combination)

So I will add a :+1: to this feature.

Stefano

I'm all for this feature, but Is it really that better to do Product.boxes.or.gits instead of Product.where(product_type: ['boxes', 'gifts'])?

You're still getting a nice abstraction from your database.

Dorian Marié

@midu Yes a lot, than to have to write Product.normal_or_gift, Product.normal_or_box, Product.box_or_gift.

My controller should never do a SQL query with a .where(), that's way beyond what a controller should know

Carlos Antonio da Silva

You can try creating a scope for those if you don't like wheres in controllers :)

Product.with_types(:boxes, :gifts)

Or something like that.

James Sanders

@midu, @Dorian, @carlosantoniodasilva: I don't think OR'ing values against a single column is really the use case for this - you can and should use the nice convenience for IN for that, as people have pointed out - it's really needed for OR'ing across columns or across tables.

Matt Jones

I'm still not clear how this works with chaining - in particular, what does this do:

relation = Post.where("id = 1").or.where("id = 2")
relation.where(name: 'foo')

I'd naively expect it to return a query with (id = 1 OR id = 2) AND (name = 'foo'), but I think it's going to return (id = 1) OR (id = 2 AND name = 'foo') which would be pretty surprising if (for instance) the or was in a scope...

Dorian Marié

For me .where(id: 1).or.where(id: 2).where(name: 'foo') is (id = 1 OR id = 2) AND (name = 'foo') because the or is only between the two surrounding relations. By default it's always .and but maybe we should also add .and for completeness even if it does nothing. So .where(id: 1).or.where(id: 2).and.where(name: 'foo') would be more clear.

Matt Jones

@Dorian - I'm personally -1 on the .or. syntax in general, because one way or another something isn't expressible. If where(id: 1).or.where(id: 2).and.where(name: 'foo') produces (id = 1 OR id = 2) AND name = 'foo', then statements like (id = 1) OR (id = 2 AND name = 'foo' require that the query being used for the or be produced in one shot (as where(id: 1).or.where(id: 2, name: 'foo') - which breaks the expected behavior that where(id: 1).where(name: 'foo') is functionally identical to where(id: 1, name: 'foo').

I'll try to write some test cases to illustrate my concerns this weekend.

Dorian Marié

@al2o3cr good point

Matthew Hensrud

I think the chaining issue is a killer here. Any complex OR relationship should be thought of as a whole and it should be 100% clear how grouping occurs with queries like this.

You need complex queries like this, USE SQUEEL.

Gael Muller

You have to think about how chaining works to be able to understand what behavior or will have. Keep in mind that:

relation.where(id: 1).or(id: 2).where(id: 3)

is equivalent to:

relation = relation.where(id: 1)
relation = relation.or(id: 2)
relation = relation.where(id: 3)

When trying to understand what this will do, you should always use the second block to decompose the chaining. In this case, it is pretty obvious that or applies to everything on the left, and only one element on the right (we have no way of knowing if there will be more than one).

A few examples taken from this thread:

>> relation = Post.where(id: 1).or.where(id: 2)
>> relation.where(name: "foo").to_sql
=> "SELECT \"posts\".* FROM \"posts\"  WHERE \"posts\".\"name\" = 'foo' AND 
((\"posts\".\"id\" = 1 OR \"posts\".\"id\" = 2))"

As noted by @al2o3cr we cannot produce (id = 1) OR (id = 2 AND name = 'foo') using .where(id: 1).or.where(id: 2).where(name: 'foo'). This is why the or method is able to take a Relation as an argument:

>> Post.where(id: 1).or(Post.where(id: 2).where(name: "foo")).to_sql
=> "SELECT \"posts\".* FROM \"posts\"  WHERE ((\"posts\".\"id\" = 1 OR \"posts\".\"id\" = 2 
AND \"posts\".\"name\" = 'foo'))"

Please not that in SQL, AND takes precedence over OR.

There is another case that can be not as elegant as we could hope: when you want to apply or only to one element on the left. Let's say you want (id = 1) AND ((id = 2) OR (title = "foo")). The naive chaining will not work:

>> relation = Post.where(id: 1)
>> relation.where(id: 2).or(title: "foo").to_sql
=> "SELECT \"posts\".* FROM \"posts\"  WHERE ((\"posts\".\"id\" = 1 AND \"posts\".\"id\" = 2 
OR \"posts\".\"title\" = 'foo'))"

You have two possibilities. The first one is to change the order, to first call the or:

>> relation = Post.where(id: 2).or(title: "foo")
>> relation.where(id: 1).to_sql
=> "SELECT \"posts\".* FROM \"posts\"  WHERE \"posts\".\"id\" = 1 AND ((\"posts\".\"id\" = 2 
OR \"posts\".\"title\" = 'foo'))"

Or, if you don't really get to manage what comes first:

>> relation = Post.where(id: 1)
>> relation.merge(Post.where(id: 2).or(title: "foo")).to_sql
=> "SELECT \"posts\".* FROM \"posts\"  WHERE \"posts\".\"id\" = 1 AND ((\"posts\".\"id\" = 2 
OR \"posts\".\"title\" = 'foo'))"

This is where we could probably use something like and that would allow use to write .where(id: 1).and(Post.where(id: 2).or(title: "foo"))

Deleted user

@al2o3cr test cases would be great! it also solves a problem that is quite complex to solve with just AREL or plain old SQL(SQL variant being simple).

for example, imagine:

class Car < ActiveRecord::LOL
  has_many :registrations
  def self.registered_on(start, finish)
    joins(:registrations).where(registrations: {value: start .. finish})
  end
end
Car.registered_on(1.days.ago, 10.days.ago).or.registered_on(65.days.ago, 68.days.ago)

unless i'm missing something, AR doesn't make that very easy right now unless you fallback to AREL - which is overly complicated to solve this problem and leaves ActiveRecord altogether. other option being a really long SQL string.

Deleted user

@gaelmuller is the above query possible with your pull request? do I need to do anything different?

Gael Muller

Yes, @robgleeson it should work.

Deleted user

awesome, thank you.

br3nt

Here's another example that needs or: Gist: Advanced Search utilising scopes (problem with match on any).

The PatientSearch#results method should be able to select between matching any of the criteria (results.or(query)), or all of the criteria (results.where(query)).

To make this work in rails currently, all query methods in PatientSearch would need to return arel instead of a relation so that they can be ORed together. This has a flow on effect such that any scopes used would also need to be rewritten as methods that return arel. Basically, throw the ActiveRecord query dsl and all its conveniences out the window.

This wouldn't be an issue if or existed.

  queries.each do |query|
    next if !query # skip if no query
    if match_criteria == 'all'
      results = results.where(query)  # each search gets ANDed
    elsif match_criteria == 'any'
      results = results.or(query)  # each search gets ORed
    end
  end
Stefano

The problem is more that all these chainable methods are an incitation to violating the law of demeter, which we all know is wrong.

Volodymyr Shatsky

@midu I would say that the law of demeter isn't an issue here. You can think of it as the builder pattern: we don't rely on object's internals, or dependencies of dependency, if you wish; instead we add pieces to one object. The returning object is always the same. In alternative syntax it could look like

query = Query.new
query.add_where_clause!(...)
query.add_where_clause!(...)
br3nt

@midu, the example I provided is not an 'incitation to violating the law of demeter'. In fact it should quite cleanly build a query if there was an or operator.

The example I provided is a very legitimate example of where or requires support from ActiveRecord.

Besides, so long as its implementation is consistent with the rest of the dsl it should be the programmers prerogative to decide how they build queries.

br3nt

@midu, the law of demeter (and all other concerns) is secondary to a much larger issue.

Here is why an 'or` operator should be supported by the ActiveRecord query dsl:

  • and and or are the core building blocks of mathematical logic and set theory
  • The SQL grammar is based on set theory and mathematical logic theory
  • SQL builds up a logic statement that defines the set to be returned
  • Excluding the 'or' operator from ActiveRecord's query dsl is denying the use of a foundational logic operator

This fact should be considered above all else as it is at the heart of SQL and all the mathematical theory that it is built around.

(on a side note, imagine if ruby didn't have an || operator)

Pavel Kotlyar

+1

Deleted user

sounds good to me, but i have no interest in rails/AR.

Deleted user

all the +1's are also strange. you would swear someone was using a bot-net to sway public opinion.

Deleted user

"law of demeter", oh get on that, +1 +1 +1 +1.

Matthew Draper matthewd self-assigned this
Felipe Zavan

+1

YI FU

would like to see this feature be added

Luke Griffiths

@br3nt said it right: OR is a fundamental mathematical operation which deserves support. Without using OR, our actions are limited.

Rafael Mendonça França rafaelfranca locked and limited conversation to collaborators
Rafael Mendonça França

I locked the conversation. We already have enough reasons to thinks about it so we don't need more support comments like "+1" or "I agree with this". We are going to review and if we decide to merge we will reopen the discussion.

Rafael Mendonça França rafaelfranca modified the milestone: 4.2.0, 5.0.0
Sean Griffin
Collaborator

Closing in favor of #18706 or #16052

Sean Griffin sgrif closed this
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Commits on Apr 24, 2013
  1. Gael Muller

    Added #or to ActiveRecord::Relation

    gaelmuller authored
    ActiveRecord::Relation#or 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, contraty to the default behaviour that uses AND.
    
    ActiveRecord::Relation#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 ActiveRecord::Relation#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
    
    ActiveRecord::Relation#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'))
This page is out of date. Refresh to see the latest.
12 activerecord/CHANGELOG.md
View
@@ -19,6 +19,18 @@
*Alexander Grebennik*
+* Added the `#or` method on ActiveRecord::Relation, in order to use the OR
Paul Nikitochkin
pftg added a note

Please move changelog entry to the first line

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ operand when joining WHERE clauses.
+
+ Examples:
+
+ Post.where('id = 1').or.where('id = 2')
+ Post.where('id = 1').or.containing_the_letter_a
+ Post.where('id = 1').or(Post.where('id = 2')
+ Post.where('id = 1').or('id = 2')
+
+ *Gael Muller*
+
* Added a state instance variable to each transaction. Will allow other objects
to know whether a transaction has been committed or rolled back.
2  activerecord/lib/active_record/querying.rb
View
@@ -9,7 +9,7 @@ module Querying
delegate :find_each, :find_in_batches, :to => :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :references, :none, :to => :all
+ :having, :create_with, :uniq, :references, :none, :or, :to => :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
# Executes a custom SQL query against your database and returns all the results. The results will
88 activerecord/lib/active_record/relation/query_methods.rb
View
@@ -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)]
+ 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)
52 activerecord/test/cases/relation/or_test.rb
View
@@ -0,0 +1,52 @@
+require "cases/helper"
Paul Nikitochkin
pftg added a note

to be consistent with other code I think better to use: require 'cases/helper'

Deleted user
ghost added a note

i love your attention to detail but that's extreme :)

Rafael Mendonça França Owner

I don't see as extreme. I'm not saying that you need to change but this make totally sense to me.

Deleted user
ghost added a note

i only see it as extreme because it doesn't really have any practical implications.
if i had to apply logic to this I think double quotes because I can easily use string interpolation there too but it seems like a small thing to worry about. i dont know how precise rails contributions need to be.

Rafael Mendonça França Owner

Single or double quotes are fine, but like I said don't need to change.

I Don't know how precise rails contributions need to be.

Deleted user
ghost added a note

ty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+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')
Paul Nikitochkin
pftg added a note

p = Post.where('id = 1').load will use less lines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ p.load
+ assert_equal p.loaded?, true
+ assert_equal expected, p.or('id = 2').to_a
+ end
+
+ end
+end
Something went wrong with that request. Please try again.