How to query the result of a method in Ruby (rails) - ruby-on-rails

I'm struggling with a particular feature.
In my users model I have a method to work out their age based on the current date less their first order date.
I'd like to be able to find all users who are older than X days. I can find active users by querying a column called state for 'active' users. But I'm unsure how to query the result of the age method to find users older than X.
Does anyone have any ideas?
Many thanks and seasons greetings.
**Edit
In postgresql I would write;
WITH
firstbill as (
SELECT
DISTINCT(user_id) as customer,
DATE(MIN(billed_at)) as first_order
FROM orders
WHERE state = 'shipped'
GROUP BY 1
ORDER BY 1)
SELECT count*
FROM
(SELECT *, (current_date - first_order) as age
FROM firstbill
JOIN users on users.id = customer) as t2
WHERE age >= 21
I have tried using User.find_by_sql["above query"] but that returns an array not activerecord relation which makes any further joins a little harder

You cannot really query for the return value of a method. Because to do so, you need to load all users and then call that method on every user, like this User.all.select(&:your_method?). That will be very slow if you have many users.
But for your particular example you can write something like this to let the database return the correct users (assuming you have a first_ordercolumn on your user):
User.where('first_order <= ?', 90.days.ago)
or
User.where('first_order <= ?', 1.month.ago)
I think the following startment should return the same users than your Postgresql example:
User.
select('users.*, MIN(DATE(orders.billed_at)) AS first_order_on').
joins('orders ON orders.user_id = users.id'). # just `(:orders)` with `has_many :order` on User
where('orders.state = ?', 'shipped').
group('users.id').
having('first_order_on <= ?', 21.days.ago.to_date)

Solved by using
scope :acquired, User.joins(:orders).where("orders.state = ?", "shipped")
scope :older_than_age, ->(age) {
acquired.group("users.id").having("(current_date - date(min(orders.shipped_at))) >= ?", age)
}

Related

How to query a ActiveRecord Relation for created_by field

So i'm currently using the following command to join and query my tables - looking for an OrderItem amongst my Orders where the orderable_id = applicable_product_item_id the total_price = 0 and the buyer_id = current_user
Order.joins(:items)
.where(order_items: {id: OrderItem.where(orderable_id: applicable_product_item_id)})
.where(total_price: 0)
.where(buyer_id: current_user)
This all works fine, but now i want to query further and i want to know if the order that it has found has a created_at date > searchable_created_by_date
i've tried using another .where in the query as well as selecting the .first in the array and further querying that i.e. query = above_query.first
then
query.where("created_at > ?", searchable_created_by_date)
but i get
Undefined method where for #<Order:0x007fbc8d8edf90>
furman87's comment sounds right to me:
You'll have to specify the table in your where clause -- .where("orders.created_at > ?", searchable_created_by_date)
You might also try:
Order.
where(total_price: 0).
where(buyer_id: current_user).
where("created_at > ?", searchable_created_by_date).
joins(:order_items).
where(order_items: {id: OrderItem.where(orderable_id: applicable_product_item_id)})
I think putting the created_at statement before the joins statement will disambiguate the query - but I'm not 100% sure.
Also, I would have thought that you would have done joins(:order_items). But, I suppose that depends on how you have your associations set up. If joins(:items) works for you, then more power to you! (And ignore the comment.)

ActiveRecord query performance, performing a where after initial query has been executed

I have this query:
absences = Absence.joins(:user).where('users.company_id = ?', #company.id).where('"from" <= ? and "to" >= ?', self.date, self.date).group('user_id').select('user_id, sum(hours) as hours')
This will return user_id's with a total of hours.
Now I need to to loop through all users of the company and do some calculations.
company.users.each do |user|
tc = TimeCheck.find_or_initialize_by(:user_id => user.id, :date => self.date)
tc.expected_hours = user.working_hours - absences.where('user_id = ?', user.id).first.hours
end
For performance reasons I want to have only one query to the absences table (the first one) and afterwards to look in memory for the correct user. How do I best accomplish this? I believe by default absences will be a ActiveRecord::Relation and not a result set. Is there a command I can use to instruct activerecord to execute the query, and afterwards search in memory?
Or do I need to store absences as array or hash first?
One SQL optimization you could make is:
change:
absences.where('user_id = ?', user.id).first.hours
to:
absences.detect { |u| u.user_id == user.id }.hours
Also, You might not need to loop through company.users. You may be able to loop through absences instead, depending on the business requirements.

Rails Arel Cohort Analysis

I'm trying to do a cohort analysis query in Rails but running into trouble with the correct way to group by the last action date.
I want to end up with rows of the following data for something like this: http://www.quickcohort.com/
count first_action last_action
for all users who registered in the last year. first_action and last_action are truncated to the nearest month.
Getting the counts grouped by the first_action is easy enough, but when I try to extend it to include the last_action I encounter
ActiveRecord::StatementInvalid: PGError: ERROR: aggregates not allowed in GROUP BY clause
Here's what I have so far
User
.select("COUNT(*) AS count,
date_trunc('month', users.created_at) AS first_action,
MAX(date_trunc('month', visits.created_at)) AS last_action # <= Problem
")
.joins(:visits)
.group("first_action, last_action") # TODO: Subquery ?
.order("first_action ASC, last_action ASC")
.where("users.created_at >= date_trunc('month', CAST(? AS timestamp))", 12.months.ago)
The visits table tracks all visits users make to the site. Using the latest visit as the last action seems like it should be easy, but I'm having trouble forming it into SQL.
I'm also open to other solutions if there are better ways, but it seems like a single SQL query would be most performant.
I think you need to do this in a subquery. Something like:
select first_action, last_action, count(1)
from (
select
date_trunc('month', visits.created_at) as first_action,
max(date_trunc('month', visits.created_at)) as last_action
from visits
join users on users.id = visits.user_id
where users.created_at >= ?
group by user_id
)
group by first_action, last_action;
I'm not sure what the most elegant way would be to do this in ARel, but I think it'd be something like this. (Might just be easier to use the SQL directly.)
def date_trunc_month(field)
Arel::Nodes::NamedFunction.new(
'date_trunc', [Arel.sql("'month'"), field])
end
def max(*expressions)
Arel::Nodes::Max.new(expressions)
end
users = User.arel_table
visits = Visit.arel_table
user_visits = visits.
join(users).on(visits[:user_id].eq(users[:id])).
where(users[:created_at].gteq(12.months)).
group(users[:id]).
project(
users[:id],
date_trunc_month(visits[:created_at]).as('first_visit'),
max(date_trunc_month(visits[:created_at])).as('last_visit')
).
as('user_visits')
cohort_data = users.
join(user_visits).on(users[:id].eq(user_visits[:id])).
group(user_visits[:first_visit], user_visits[:last_visit]).
project(
user_visits[:first_visit],
user_visits[:last_visit],
Arel::Nodes::Count.new([1]).as('count')
)

Sequel -- How To Construct This Query?

I have a users table, which has a one-to-many relationship with a user_purchases table via the foreign key user_id. That is, each user can make many purchases (or may have none, in which case he will have no entries in the user_purchases table).
user_purchases has only one other field that is of interest here, which is purchase_date.
I am trying to write a Sequel ORM statement that will return a dataset with the following columns:
user_id
date of the users SECOND purchase, if it exists
So users who have not made at least 2 purchases will not appear in this dataset. What is the best way to write this Sequel statement?
Please note I am looking for a dataset with ALL users returned who have >= 2 purchases
Thanks!
EDIT FOR CLARITY
Here is a similar statement I wrote to get users and their first purchase date (as opposed to 2nd purchase date, which I am asking for help with in the current post):
DB[:users].join(:user_purchases, :user_id => :id)
.select{[:user_id, min(:purchase_date)]}
.group(:user_id)
You don't seem to be worried about the dates, just the counts so
DB[:user_purchases].group_and_count(:user_id).having(:count > 1).all
will return a list of user_ids and counts where the count (of purchases) is >= 2. Something like
[{:count=>2, :user_id=>1}, {:count=>7, :user_id=>2}, {:count=>2, :user_id=>3}, ...]
If you want to get the users with that, the easiest way with Sequel is probably to extract just the list of user_ids and feed that back into another query:
DB[:users].where(:id => DB[:user_purchases].group_and_count(:user_id).
having(:count > 1).all.map{|row| row[:user_id]}).all
Edit:
I felt like there should be a more succinct way and then I saw this answer (from Sequel author Jeremy Evans) to another question using select_group and select_more : https://stackoverflow.com/a/10886982/131226
This should do it without the subselect:
DB[:users].
left_join(:user_purchases, :user_id=>:id).
select_group(:id).
select_more{count(:purchase_date).as(:purchase_count)}.
having(:purchase_count > 1)
It generates this SQL
SELECT `id`, count(`purchase_date`) AS 'purchase_count'
FROM `users` LEFT JOIN `user_purchases`
ON (`user_purchases`.`user_id` = `users`.`id`)
GROUP BY `id` HAVING (`purchase_count` > 1)"
Generally, this could be the SQL query that you need:
SELECT u.id, up1.purchase_date FROM users u
LEFT JOIN user_purchases up1 ON u.id = up1.user_id
LEFT JOIN user_purchases up2 ON u.id = up2.user_id AND up2.purchase_date < up1.purchase_date
GROUP BY u.id, up1.purchase_date
HAVING COUNT(up2.purchase_date) = 1;
Try converting that to sequel, if you don't get any better answers.
The date of the user's second purchase would be the second row retrieved if you do an order_by(:purchase_date) as part of your query.
To access that, do a limit(2) to constrain the query to two results then take the [-1] (or last) one. So, if you're not using models and are working with datasets only, and know the user_id you're interested in, your (untested) query would be:
DB[:user_purchases].where(:user_id => user_id).order_by(:user_purchases__purchase_date).limit(2)[-1]
Here's some output from Sequel's console:
DB[:user_purchases].where(:user_id => 1).order_by(:purchase_date).limit(2).sql
=> "SELECT * FROM user_purchases WHERE (user_id = 1) ORDER BY purchase_date LIMIT 2"
Add the appropriate select clause:
.select(:user_id, :purchase_date)
and you should be done:
DB[:user_purchases].select(:user_id, :purchase_date).where(:user_id => 1).order_by(:purchase_date).limit(2).sql
=> "SELECT user_id, purchase_date FROM user_purchases WHERE (user_id = 1) ORDER BY purchase_date LIMIT 2"

Rails 3. How to perform a "where" query by a virtual attribute?

I have two models: ScheduledCourse and ScheduledSession.
scheduled_course has_many scheduled_sessions
scheduled_session belongs_to scheduled_course
ScheduledCourse has a virtual attribute...
def start_at
s = ScheduledSession.where("scheduled_course_id = ?", self.id).order("happening_at ASC").limit(1)
s[0].happening_at
end
... the start_at virtual attribute checks all the ScheduledSessions that belongs to the ScheduledCourse and it picks the earliest one. So start_at is the date when the first session happens.
Now I need to write in the controller so get only the records that start today and go into the future. Also I need to write another query that gets only past courses.
I can't do the following because start_at is a virtual attribute
#scheduled_courses = ScheduledCourse.where('start_at >= ?', Date.today).page(params[:page])
#scheduled_courses = ScheduledCourse.where('start_at <= ?', Date.today)
SQLite3::SQLException: no such column: start_at: SELECT "scheduled_courses".* FROM "scheduled_courses" WHERE (start_at >= '2012-03-13') LIMIT 25 OFFSET 0
You can't perform SQL queries on columns that aren't in the database. You should consider making this a real database column if you intend to do queries on it instead of a fake column; but if you want to select items from this collection, you can still do so. You just have to do it in Ruby.
ScheduledCourse.page(params).find_all {|s| s.start_at >= Date.today}
Veraticus is right; You cannot use virtual attributes in queries.
However, I think you could just do:
ScheduledCourse.joins(:scheduled_sessions).where('scheduled_courses.happening_at >= ?', Date.today)
It will join the tables together by matching ids, and then you can look at the 'happening_at' column, which is what your 'start_at' attribute really is.
Disclaimer: Untested, but should work.
I wonder if this would be solved by a subquery ( the subquery being to find the earliest date first). If so, perhaps the solution here might help point in a useful direction...

Resources