How does this ActiveRecord 'where' clause work? - ruby-on-rails

I have this statement:
myuser.orders.exists?(['(orderstatus = ?) ', statusid])
It returns true since there is an orderstatus that matches the statusid.
Next I have:
myuser.orders.where('id not in (?)', nil).exists?(['(orderstatus = ?) ', statusid])
This returns false where I thought it might return true since there are no ids that are nil.
Then I have:
myuser.orders.where(nil).exists?(['(orderstatus = ?) ', statusid])
This returns true.
My question is why does the middle statement return false? It doesn't complain or throw any errors. I guess I'm using nil wrong, but can someone explain?

You're having trouble with SQL's NULL. The where in the middle one:
where('id not in (?)', nil)
becomes this SQL:
id not in (null)
and that's equivalent to this:
id != null
But the result of id != null is neither true nor false, the result is NULL and NULL in a boolean context is false; in fact, x = null and x != null result in NULL for all x (even when x itself is NULL); for example, in PostgreSQL:
=> select coalesce((11 = null)::text, '-NULL-');
coalesce
----------
-NULL-
(1 row)
=> select coalesce((11 != null)::text, '-NULL-');
coalesce
----------
-NULL-
(1 row)
=> select coalesce((null = null)::text, '-NULL-');
coalesce
----------
-NULL-
(1 row)
=> select coalesce((null != null)::text, '-NULL-');
coalesce
----------
-NULL-
(1 row)
MySQL and every other reasonably compliant database will do the same thing (with possibly different casting requirements to make the NULL obvious).
The result is that where(id not in (?)', nil) always yields an empty set and your existence check will always fail on an empty set.
If you want to say "all the rows where id is not NULL" then you want to say:
where('id is not null')
If your id is a primary key (as it almost certainly is), then id will never be NULL and you can leave that where out completely.
When you hand where just a nil:
where(nil)
where's argument parsing logic will ignore the nil completely and where(nil) will be the same as where() and where() does nothing at all to the query. The result is that the first and third queries are identical as far as the database is concerned.

Related

Rewhere or unscope a query containing an array condition 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

How can I query to see if all records in a child model have the same value for an attribute?

First off, I know this is really basic stuff for most of you but I'm still learning and I would appreciate any help.
I have a boolean column in my child table. I am trying to query all the child records associated with a parent to see whether the value of the boolean column is true for all those child records or not.
What I have now is this:
if Parent.children.count(:conditions => [ 'boolean_column = ?', true ]) == Parent.children.count
return true
My logic with that being that if the count of all the associated child records is equal to the count of the associated child records where the boolean column is true, then that should return true, but of course it doesn't work.
What is the rails way of doing this, if there is one?
You can achieve this by doing only 1 query. But to do, you need to make sure first that the boolean column should never have NULL, except true or false. So adding NOT NULL constraint will make sure that column will never have NULL. After that you can run the below query:
hash = Children.
group(:boolean_column).
where(parent_id: 12).
count(:boolean_column)
12 should be replaced by your actual id of the parent. Now, do check if hash has the false as a key, like hash.has_key?(false). If there is no false key, you can assert that all children has true to the boolean column.
You can do it as
Parent.children.where("boolean_column = ?", true).count == Parent.children.count

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")

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.

Rails returns a nil, when searching with find_by

I'm beginning to learn RoR, but i've a problem which i don't understand. With Product.find :all returns all the records from DB. But if i want to find_by_gender(1) (or even 2) it returns a nil, i'm certain that the db contains products with a gender
My code controller:
gender = params[:gender].to_i
#search_results = Product.find_by_gender(gender)
this returns a nill,
What am i doing wrong?
Greetings!
find_by_... returns either first record or nil if none found, find_all_by_... returns all records that match (or empty array if none). In your case nil means no records found with gender = 1.
Verify your data first!
Look at some sample records:
Do something like:
Product.all(:limit => 5).each {|product| product.id.to_s + product.gender}
or go into sql
sql> select id, gender from products where id < 6;
If you are to verify what the gender values are you can then create named scopes in your model for those conditions, e.g. (rails3)
(Product Model - app/models/product.rb)
scope :male where(:gender) = male_value # i.e. 1 or 'M' or 'Male' or whatever
scope :female where(:gender) = female_value # i.e. '2' or 'F' or whatever
Which will then you let write Products.male or Products.female !
Final note - should gender be in your users table? , or is this for male / female specific products?
in rails console execute
Product.pluck(:gender)
And u will know that values does it have in AR(i think true and false), so u have to use query Product.find_by_gender(true)

Resources