Confusing result when using where.not with Rails - ruby-on-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")

Related

Using a scope to order by 2 deeply nested fields

So i want to order a list of PickUp by 2 deeply nested date fields. I have to order by 2 fields because the latest_eta is sometimes null but is most accurate. If its null I want to fall back on the scheduled_at field
Here is my current incorrect scope:
scope :order_by_pickup, -> {
joins(transit: :pickup)
.order(TripStop.latest_eta, TripStop.scheduled_at)
}
This does order by latest_eta never the scheduled_at. Any ideas on how to do this?
You can use coalesce method. It returns the first non-null argument.
scope :order_by_pickup, -> {
joins(transit: :pickup)
.order('coalesce(trip_stops.latest_eta, trip_stops.scheduled_at) desc')
}
You can do it with raw SQL
Assuming I got your association correctly:
scope :order_by_pickup, -> {
joins(transit: :pickup)
.order(Arel.sql('CASE WHEN (latest_eta IS NULL) THEN scheduled_at ELSE latest_eta END'))
}

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

Rails 4: OR in where clause not working with querying nil

I am trying to query any Company in my database where the field visible is either NULL or true. Here is what I found on a Stackoverflow post:
#companies = Company.where('visible=? OR visible=?', nil, true).page(params[:page]).per(10)
Somehow though, this does not seem to work for querying nil. When I use this code, displaying all companies where visible is nil works very well though.
#companies = Company.where('visible' => nil).page(params[:page]).per(10)
I would very much appreciate any ideas here.
Thanks!
EDIT:
This still displays only companies where visible is nil:
#companies = Company.where('visible is ? OR visible=?', nil, true).page(params[:page]).per(10)
That's because
Company.where('visible=?', nil) makes query:
Company Load (0.3ms) SELECT companies.* FROM companies WHERE
(visible = NULL)
In SQL, to compare with NULL, = doesn't work. It requires IS instead.
Company.where('visible is ?', nil) should do the trick for you. Add or statement along with it.
Company Load (0.3ms) SELECT companies.* FROM companies WHERE
(visible is NULL)
OR, the perfect way:
Company.where(:visible => [true, nil])
The problem is that you have no NOT NULL constraint in your DB.
You can also use conditional expressions(CASE) or function COALESCE.

Sql is null in ActiveRecord query

I have class method to get posts but now sure how to resolve it.
So, somebody can set flag on post, but in meantime post doesn't have any flag :
Post.find(3).flag #nil
This is self method for getting posts :
def self.for_review
joins(:flag).where('need_approval = ? and question = ? and DATE(posts.created_at) = ?', "true", "false", Time.now.to_date)
.where('flags.need_check = ? or flags IS NULL', 'false')
end
Problem is with second where condition
where('flags.need_check = ? or flag IS NULL', 'false')
Because flag is NULL doesn't work .
Is anybody knows what's problem ?
Thanks
joins uses an INNER JOIN by default; this will only return posts that do have at least one flag. You need a LEFT JOIN. Assuming a Post has_many flags:
joins('LEFT JOIN flags ON posts.id = flags.post_id').where(...).where('flags.need_check = ? OR flags.id IS NULL', false)
Also drop the quotes around true and false, unless those fields are indeed strings with the literal values "true" and "false".
THe problem is joins(:flag). It defaults to inner join, so all the posts without flags will be rejected. You need includes(:flag).references(:flag) instead.
Also flags IS NULL makes no sense as there is no column called flags. Instead do flags.id IS NULL

How does this ActiveRecord 'where' clause work?

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.

Resources