Using a scope to order by 2 deeply nested fields - ruby-on-rails

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'))
}

Related

Rails scope method returns WhereChain instead of array

In my Deal class, I've added a scope method:
scope :current, -> { where {|d| d.end_date > Date.today} }
It returns the right data, but rather than returning an array that I can count, iterate, etc, it returns an ActiveRecord::QueryMethods::WhereChain, and I can't do anything with that.
I notice that a call to Deal.current starts with "#scope=":
Deal.current
Deal Load (1.0ms) SELECT "deals".* FROM "deals"
=> #<ActiveRecord::QueryMethods::WhereChain:0x00007fbc61228038
#scope=
[#<Deal:0x00007fbc5fdb2400
id: 7,
I suppose if I knew how to use a "normal" where statement for comparison (rather than equality) such as where(end_date>Date.today) that would band-aid my problem, since a scope method using a simple where statement like scope :posted, -> { where.not(posted_date:nil) } returns a normal useable ActiveRecord.Relation, but I do want to figure out how to use more complex where queries like this one correctly.
You could use as following:
scope :current, -> { where('end_date > ?', Date.today) }
The block-syntax for a where-call, does not exist in Rails.
You could also look into arel (https://robots.thoughtbot.com/using-arel-to-compose-sql-queries);
deal = Deal.arel_table
Deal.where(deal[:end_date].gt(Date.today))

Activerecord scope query on hstore attributes, check if attribute is nil?

I have a column in my Subscription model called expiry_date that tracks when a subscription will expire. I also have hstore column called properties which has an attribute called expiry_email_sent that is used as a flag to determine if an email has been sent.
store_accessor :properties, :expiry_email_sent
I have a scope as below to extract all records that have expiry_date between today and the next 5 days. It also fetches only those records that have properties as nil, or those for which expiry_email_sent does not exist. I want to addd a check to fetch records for which expiry_email_sent is nil if it exists. How would I add that in the scope below?
scope :expiry_soon_email, -> { where("expiry_date >= ? AND expiry_date <= ? AND (properties IS NULL OR EXIST(properties, 'expiry_email_sent') = FALSE", Time.zone.now.to_date, Time.zone.now.to_date + 5.days) }
You may use Postgres function defined(hstore,text) which means
does hstore contain non-NULL value for key?
Your scope should looks like
scope :expiry_soon_email, -> { where("expiry_date >= ? AND expiry_date <= ? AND (properties IS NULL OR DEFINED(properties, 'expiry_email_sent') = FALSE", Time.zone.now.to_date, Time.zone.now.to_date + 5.days) }
See details about hstore functions in Postgres documentation.

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

Scope on number of items in associated model

Suppose we have a Tag model with many associated Post (or none) via a has_many association.
Is there an efficient way to select only the tags that do have a tag.posts.size > 0 via a scope ?
Something that would behave as follows:
scope :do_have_posts, -> { where("self.posts.count > 0") } #pseudo-code
Thanks.
This should only return you the tags with posts since rails does an inner join by default
scope :with_posts, -> { joins(:posts).uniq }

Rails 3 - nested select and ".last"

I have a scenario which seems to be should be something addressed on the model side of things but can't figure out a combination of Activerecord methods that works. The relevant parts of my db are as below:
Part:
id
part_id
created_at
...
Parttest:
id
part_id
pass (boolean)
created_at
A Part can have many Parttests and a Parttest belongs to a single part (setup in the relevant models as has_many and belongs_to).
In it's life - a part can be tested many times and can pass, fail, pass etc.
Using rails / activerecord I would like to retrieve the recordset of Parts where the last (most recent) Parttest is a pass. Earlier records of parttest should not be taken into consideration and are for historical use only.
I've tried nested selects without much success. I could always code the logic in the controller to check each Part object in Part.all.each and push those into an array but this doesn't feel like the right way to do it. All help much appreciated :)
Edit: Misread your post, you want to do something like this reference
Part.joins(:parttests).where(parttests: {pass: true}) will give you all parts who have passing tests
try Part.joins(:parttests).last.where(parttests: {pass: true})
Using a scope
class Parttest
scope :passed_tests, -> { where(pass: true) }
end
#part = Part.find(1) # Fetch part instance
#part_tests = #part.parttests.passed_tests.last # Fetch its tests
Or using a condition on the association:
class Part
has_many :passed_tests, class_name: 'Parttest', -> { where(pass: true) }
end
#part_tests = #part.passed_tests.last
Update
Now that I have understood the question correctly. How about this?
class Part
has_many :passed_tests, class_name: 'Parttest', -> { where(pass: true) }
scope :with_last_passed_test, -> { joins(:passed_tests).last }
end
Part.with_last_passed_test
I had originally posted a lengthy SQL query as one option, but that suggestion wasn't very clean so I've removed it. Here is a way to break this down into a few small pieces.
Say we start with these Parttest rows:
id part_id created_at pass
1 1 2014-04-26 true
2 1 2014-04-27 false
3 2 2014-04-26 false
4 2 2014-04-27 true
1) First, we want to find the most recent Parttest row for each Part. We can do this with:
# Parttest model
def self.most_recent
max_rows = select("part_id, MAX(created_at) as max_created_at").group(:part_id)
joins("INNER JOIN (#{max_rows.to_sql}) AS max_rows
ON parttests.part_id = max_rows.part_id
AND parttests.created_at = max_rows.max_created_at")
end
The select will grab these values:
part_id created_at
1 2014-04-27
2 2014-04-27
And the join will connect us back up with the other Parttest columns:
id part_id created_at pass
2 1 2014-04-27 false
4 2 2014-04-27 true
2) Then we want to only keep the passing tests. We can do this with:
# Parttest model
scope :passed, -> { where(pass: true) }
Combining this with (1), we can get the most recent tests only if they were passing tests by calling:
Parttest.most_recent.passed
This would only include return this row from our example:
id part_id created_at pass
4 2 2014-04-27 true
3) Then to find all Parts where the most recent test was passing we can provide this scope:
# Part model
scope :most_recent_test_passed, -> { where("id IN (?)", PartTest.most_recent.passed.map(&:part_id)) }

Resources