Rewhere or unscope a query containing an array condition on Rails - ruby-on-rails

I'm trying to rewhere or unscope a query, where the original condition cannot be written using hash condition:
Reservation.where('block_id IS NULL OR block_id != ?', 'something')
> SELECT `reservations`.* FROM `reservations` WHERE (block_id IS NULL OR block_id != 'something')
Trying to rewhere doesn't work:
Reservation.where('block_id IS NULL OR block_id != ?', 'something').rewhere(block_id: 'anything')
> SELECT `reservations`.* FROM `reservations` WHERE (block_id IS NULL OR block_id != 'something') AND `reservations`.`block_id` = 'anything'
But this example with hash condition would work:
Reservation.where.not(block_id: 'something').rewhere(block_id: 'anything')
> SELECT `reservations`.* FROM `reservations` WHERE `reservations`.`block_id` = 'anything'
I understand that this is probably because on the array condition rails doesn't know which column I'm invoking a where, and therefore rewhere won't find anything to replace.
Is there any way to explicitly tell which column I'm filtering in an array condition? or rewrite the first query (IS NULL OR != value) with hash condition?
Note: Please don't suggest unscoped, as I'm trying to unscope/rewhere only this specific condition, not the whole query.
Thanks!

Sorry it wasn't clear that you had other where clauses that you wanted to keep. You could access the array of where clauses using relations.values[:where] and manipulate it, something like:
Reservation.where('block_id IS NULL OR block_id != ?', 'something')
.tap do |relation|
# Depending on your version of Rails you can do
where_values = relation.where_values
# Or
where_values = relation.values[:where]
# With the first probably being better
where_values.delete_if { |where| ... }
end
.where(block_id: 'anything')
aka hacking

Related

Confusing result when using where.not with Rails

I've got an orders model with a payment status string field. Ordinarily this field should be populated but sometimes it's nil. I have the following scope
scope :not_pending, -> { where.not(payment_status: 'pending') }
In one of my tests, the payment_status is nil for an order but I still get an empty result for the scope. If I change it to remove the .not, I get an empty result again so I'm a bit confused.
EDIT - Added SQL
"SELECT "orders".* FROM "orders" INNER JOIN "order_compilations" ON "orders"."id" = "order_compilations"."order_id" WHERE "order_compilations"."menu_group_id" = 3724 AND ("orders"."payment_status" != 'pending')"
Just add nil along, then it will check both
scope :not_pending, -> { where.not(payment_status: 'pending').or(payment_status: nil) }
which is equivalent to where("payment_status <> 'pending' OR payment_status IS NULL")
UPDATE
changed to include nil
This is not because of Rails but because it's how SQL actually works.
!= means something different, but still something. NULL is not something.
You can see this related issue.
In SQL, a NULL value in an expression will always evaluate the expression to false. The theory is that a NULL value is unknown, not just an empty value that you could evaluate to 0, or false.
So for example, 1 = NULL is false, but 1 != NULL is false too.
So in your case, I would probably write:
where("payment_status != 'pending' or payment_status is null")

Order of operations in ActiveRecord lookup

Approval.where("user1_approval IS NOT NULL AND user2_approval IS NOT NULL AND (user_id = ? OR approved_id = ?)", user.id, user.id)
I want the part in parenthesis to work -- either of those statements. Currently I get an error on the parenthesis when I run this query.
How can I make this query?
Do you mind to use ActiveRecord query like this ?
Approval.where.not(user1_approval:nil).where.not(user2_approval: nil).where("summoner_id = ? OR approved_id = ?", user.id, user.id)
It might reduce the potential problem in writing plain SQL.

Find records where an attribute is present

I have a User model with the attributes username, email and name.
username and email are required upon signup, but not name.
What would be the query to find all users that have filled out name (i.e. it is no nil)?
The query should be at least Rails 3.2 and 4.0 compatible.
I'm thinking something in the lines of:
User.where(name: present?)
[UPDATED 13/5/2022]
To get all records where an attribute is not present in SQL, we would write
WHERE attr IS NULL or attr = ''
an easy mistake to make is then to negate this and write
WHERE attr is not null and attr != ''
However in SQL this equates to writing
WHERE attr != ''
since the NULL value is always ignored when using the equality operator.
So this translates to rails as follows:
User.where.not(name: '')
[MY OLD ANSWER]
An empty value in a database gets a special value NULL. Testing whether is set uses the special comparator IS NULL or IS NOT NULL.
Then there still remains the possibility that an empty string was filled in, so a complete test would be
#users = User.where("name is NOT NULL and name != ''")
[UPDATED for rails 4+]
Since rails 4 we can write:
User.where.not(name: [nil, ""])
which will generate the same query. Awesome :)
present?
present? is essentially not nil and not empty?:
class Object
def present?
!blank?
end
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
end
ActiveRecord condition
In Rails 4, not conditions can be done without raw sql code.
# Both lines lead to the same result:
User.where.not(name: [nil, ""])
User.where.not(name: nil).where.not(name: "")
Since there is no raw sql code, you don't have to worry about if they work with every database adapter. In fact, this works fine for both, mysql and postgres.
to_sql
You can see how they translate to sql queries if you append .to_sql, for example in the rails console.
# rails console
User.where.not(name: [nil, ""]).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE (NOT ((\"users\".\"name\" = '' OR \"users\".\"name\" IS NULL)))"
User.where.not(name: nil).where.not(name: "").to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"name\" IS NOT NULL) AND (\"users\".\"name\" != '')"
Further Reading
[1] Rails 4 guide "ActiveRecord Query Interface"
[2] Definition of present? on github
NOT SQL queries can be built by where.not
#users = User.where.not(name: nil)
Try this:
User.where("name IS NOT NULL AND name != ?", "")
I edited my answer as per #nathavanda comments, which his answer in my opinion should be the accepted one.
You can simply do this:
User.where.not(name: '')
Because of the nature of not, it won't include records where name is nil nor empty string. See this article for more details about not and nil

Can you add clauses in a where block conditionally when using Squeel?

To start, I'm using Rails v3.2.9 with Squeel 1.0.13 and here's what I'm trying to do:
I want to search for a client using any of three pieces of identifying information - name, date of birth (dob), and social insurance number (sin). The result set must include any record that has any of the identifier - an OR of the conditions. I have done this in Squeel before and it would look something like:
scope :by_any, ->(sin, name, dob){ where{(client.sin == "#{sin}") | (client.name =~ "%#{name}%") | (client.dob == "#{dob}")} }
This works fine as long as I provide all of the identifiers. But what if I only have a name? The above scope results in:
SELECT "clients".* FROM "clients" WHERE ((("clients"."sin" IS NULL OR "clients"."name" ILIKE '%John Doe%') OR "clients"."dob" IS NULL))
This includes the set of clients where sin is null and the set of clients where dob is null along with the requested set of clients with a name like 'John Doe'.
So enter my attempt to conditionally add clauses to the where block. At first, I tried to check the values using the nil? method:
def self.by_any (sin, name, dob)
where do
(clients.sin == "#{sin}" unless sin.nil?) |
(clients.name =~ "%#{name}" unless name.nil?) |
(clients.dob == "#{dob}" unless dob.nil?)
end
which results in:
SELECT "clients".* FROM "clients" WHERE ('t')
raising many other questions, like what's the deal with that 't', but that's a tangent.
Short of writing the where clause for each permutation, is there a way I can conditionally add clauses?
So, this isn't the prettiest thing ever, but it does what you're after.
def self.by_any(sin, name, dob)
where do
[
sin.presence && clients.sin == "#{sin}",
name.presence && clients.name =~ "%#{name}",
dob.presence && clients.dob == "#{dob}"
].compact.reduce(:|)
# compact to remove the nils, reduce to combine the cases with |
end
end
Basically, [a, b, c].reduce(:f) returns (a.f(b)).f(c). In this case f, the method invoked, is the pipe, so we get (a.|(b)).|(c) which, in less confusing notation, is (a | b) | c.
It works because, in Squeel, the predicate operators (==, =~, and so on) return a Predicate node, so we can construct them independently before joining them with |.
In the case where all three are nil, it returns all records.
After eventually finding this related post, I cannibalized #bradgonesurfing 's alternate pattern to come to this solution:
def self.by_any (sin, name, dob)
queries = Array.new
queries << self.by_sin(sin) unless sin.nil?
queries << self.by_name(name) unless name.nil?
queries << self.by_dob(dob) unless dob.nil?
self.where do
queries = queries.map { |q| id.in q.select{id} }
queries.inject { |s, i| s | i }
end
end
where self.by_sin, self.by_name, and self.by_dob are simple scopes with filters. This produces something along the lines of:
SELECT *
FROM clients
WHERE clients.id IN (<subquery for sin>)
OR clients.id IN (<subquery for name>)
OR clients.id IN (<subquery for dob>)
where the subqueries are only include if their associated value is not nil.
This effectively allows me to union the appropriate scopes together as an ActiveRecord::Relation.

Does not equal conditional

I want to have a where clause with an equal and does not equal condition:
#user = User.where(:user_id => current_user.id, :author_id != current_user.id).nil? ? (render :something) : (render :somethingelse)
The above does not work:
syntax error, unexpected ')',
expecting tASSOC ...d, :user_id !=
current_user.id).nil? ? (render
:index) : (re...
If I change the second condition from != to => it will work, however.
How do I have both conditions in one where clase? Thank you
Here's how you would use Arel to generate the query "select * from users where user_id = ? and author_id != ?":
users = User.arel_table
User.where(users[:user_id]. eq(current_user.id).and(
users[:author_id].not_eq(current_user.id)))
Using Arel isn't as concise as using Hash conditions for simple conditions, but it's a lot more powerful!
Here's a link to the full list of predications (eq, not_eq, gt, lt, etc.) available with Arel.
I believe, it should be:
#user = User.where(['user_id = ? AND author_id <> ?', current_user.id, current_user.id])
render(#user ? :something : :somethingelse)
Rails 4 has this all figured out
Model.where.not(:colname => nil)
#=> returns all records whose :colname values are not nil
The syntax error is due to you attempting to use != instead of =>. The where method does not support inequality with hashed arguments, so your not equal will need to be written using array arguments.
User.where(:user_id => current_user.id).where(['users.author_id <> ?', current_user.id])
http://guides.rubyonrails.org/active_record_querying.html#hash-conditions
Only equality, range and subset checking are possible with Hash conditions.
You'll need to either drop down to straight SQL or invert and arel query, see Is there a way to invert an ActiveRecord::Relation query?
Not sure if you're aware, the not equal condition typically does not match (author_id) NULL values. You'll have to do an OR author_id IS NULL if you want that.
#users = User.where("user_id = ? AND (author_id != ? OR author_id IS NULL)",
current_user.id, current_user.id)
render(#users.present? ? :something : :somethingelse)
Also note that I'm using #users.present? because where finder returns an ActiveRecord::Relation array.

Resources