Combining distinct with another condition - ruby-on-rails

I'm migrating a Rails 3.2 app to Rails 5.1 (not before time) and I've hit a problem with a where query.
The code that works on Rails 3.2 looks like this,
sales = SalesActivity.select('DISTINCT batch_id').where('salesperson_id = ?', sales_id)
sales.find_each(batch_size: 2000) do |batchToProcess|
.....
When I run this code under Rails 5.1, it appears to cause the following error when it attempts the for_each,
ArgumentError (Primary key not included in the custom select clause):
I want to end up with an array(?) of unique batch_ids for the given salesperson_id that I can then traverse, as was working with Rails 3.2.
For reasons I don't understand, it looks like I might need to include the whole record to traverse through (my thinking being that I need to include the Primary key)?
I'm trying to rephrase the 'where', and have tried the following,
sales = SalesActivity.where(salesperson_id: sales_id).select(:batch_id).distinct
However, the combined ActiveRecordQuery applies the DISTINCT to both the salesperson_id AND the batch_id - that's #FAIL1
Also, because I'm still using a select (to let distinct know which column I want to be 'distinct') it also still only selects the batch_id column of course, which I am trying to avoid - that's #FAIL2
How can I efficiently pull all unique batch_id records for a given salesperson_id, so I can then for_each them?
Thanks!

How about:
SalesActivity.where(salesperson_id: sales_id).pluck('DISTINCT batch_id')
May need to change up the ordering of where and pluck, but pluck should return an array of the batch_ids

Related

Grouping by into a list with activerecord in rails

I need to achieve something exactly similar to How to get list of values in GROUP_BY clause? but I need to use active record query interface in rails 4.2.1.
I have only gotten so far.
Roles.where(id: 2)
.select("user_roles.id, user_roles.role, GROUP_CONCAT(DISTINCT roles.group_id SEPARATOR ',') ")
.group(:role)
But this just returns an ActiveRecord::Relationobject with a single entry that has id and role.
How do I achieve that same with active record without having to pull in all the relationships and manually building such an object?
Roles.where(id: 2) already returns the single record. You might instead start with users and join roles table doing something like this.
User.
joins(user_roles: :roles).
where('roles.id = 2').
select("user_roles.role, GROUP_CONCAT(DISTINCT roles.group_id SEPARATOR ',') ").
group(:role)
Or, if you have the model for user_roles, start with it since you nevertheless do not query anything from users.

How to get weighted average grouped by a column

I have a model Company that have columns pbr, market_cap and category.
To get averages of pbr grouped by category, I can use group method.
Company.group(:category).average(:pbr)
But there is no method for weighted average.
To get weighted averages I need to run this SQL code.
select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";
In psql this query works fine. But I don't know how to use from Rails.
sql = %q(select case when sum(market_cap) = 0 then 0 else sum(pbr * market_cap) / sum(market_cap) end as weighted_average_pbr, category AS category FROM "companies" GROUP BY "companies"."category";)
ActiveRecord::Base.connection.select_all(sql)
returns a error:
output error: #<NoMethodError: undefined method `keys' for #<Array:0x007ff441efa618>>
It would be best if I can extend Rails method so that I can use
Company.group(:category).weighted_average(:pbr)
But I heard that extending rails query is a bit tweaky, now I just want to know how to run the result of sql from Rails.
Does anyone knows how to do it?
Version
rails: 4.2.1
What version of Rails are you using? I don't get that error with Rails 4.2. In Rails 3.2 select_all used to return an Array, and in 4.2 it returns an ActiveRecord::Result. But in either case, it is correct that there is no keys method. Instead you need to call keys on each element of the Array or Result. It sounds like the problem isn't from running the query, but from what you're doing afterward.
In any case, to get the more fluent approach you've described, you could do this:
class Company
scope :weighted_average, lambda{|col|
select("companies.category").
select(<<-EOQ)
(CASE WHEN SUM(market_cap) = 0 THEN 0
ELSE SUM(#{col} * market_cap) / SUM(market_cap)
END) AS weighted_average_#{col}
EOQ
}
This will let you say Company.group(:category).weighted_average(:pbr), and you will get a collection of Company instances. Each one will have an extra weighted_average_pbr attribute, so you can do this:
Company.group(:category).weighted_average(:pbr).each do |c|
puts c.weighted_average_pbr
end
These instances will not have their normal attributes, but they will have category. That is because they do not represent individual Companies, but groups of companies with the same category. If you want to group by something else, you could parameterize the lambda to take the grouping column. In that case you might as well move the group call into the lambda too.
Now be warned that the parameter to weighted_average goes straight into your SQL query without escaping, since it is a column name. So make sure you don't pass user input to that method, or you'll have a SQL injection vulnerability. In fact I would probably put a guard inside the lambda, something like raise "NOPE" unless col =~ %r{\A[a-zA-Z0-9_]+\Z}.
The more general lesson is that you can use select to include extra SQL expressions, and have Rails magically treat those as attributes on the instances returned from the query.
Also note that unlike with select_all where you get a bunch of hashes, with this approach you get a bunch of Company instances. So again there is no keys method! :-)

tricky union query using ruby on rails/active record

I have
a = Profile.last
a.mailbox.inbox
a.mailbox.sentbox
active_conversations = [IDS OF ACTIVE CONVERSATIONS]
a.mailbox.inbox & active_conversations
returns part of what I need
I want
(a.mailbox.inbox & active_conversations) AND a.mailbox.sentbox
but I need it as SQL, so that I can order it efficiently. I want to order it by ('updated_at')
I have tried joins and other things but they don't work. The classes of (a.mailbox.inboxa and the sentbox are
ActiveRecord::Relation::ActiveRecord_Relation_Conversation
but
(a.mailbox.inbox & active_conversations)
is an array
edit
Something as simple as a.mailbox.inbox JOINS SOMEHOW a.mailbox.sentbox I should be able to work with, but I also can't seem to figure out.
Instead of doing
(a.mailbox.inbox & active_conversations)
you should be able to do
a.mailbox.inbux.where('conversations.id IN (?)', active_conversations)
I believe the Conversation class (and its underlying conversations table) should be right according to the mailboxer code.
However this gives you an ActiveRelation object instead of an array. You can transform this to pure SQL using to_sql. So I think something like this should work:
# get the SQL of both statements
inbox_sql = a.mailbox.inbux.where('conversations.id IN (?)', active_conversations).to_sql
sentbox_sql = a.mailbox.sentbox.to_sql
# use both statements in a UNION SQL statement issued on the Conversation class
Conversation.from("#{inbox_sql} UNION #{sentbox_sql} ORDER BY id AS conversations")

Ruby-on-Rails: Selecting distinct values from the model

The docs:
http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields
Clearly state that:
query = Client.select(:name).distinct
# => Returns unique names
However, when I try that in my controller, I get the following error:
undefined method `distinct' for #<ActiveRecord::Relation:0xb2f6f2cc>
To be clear, I want the distinct names, like ['George', 'Brandon'], not the clients actual records. Is there something that I am missing?
The .distinct option was added for rails 4 which is what the latest guides refer to.
Rails 2
If you are still on rails 2 you will need to use:
Client.select('distinct(name)')
Rails 3
If you are on Rails 3 you will need to use:
Client.select(:name).uniq
If you look at the equivalent section of the rails 3 guide you can see the difference between the two versions.
There are some approaches:
Rails way:
Model.select(:name).distinct
Semi-rails way
Model.select("DISTINCT ON(models.name) models.*")
The second allows you to select the first record uniqued by name, but in the whole matter, not only names.
If you do not want ActiveRecord::Relations returned, just an array of the names as strings, then use:
Client.distinct.pluck(:name)
To get an ordered result set:
Client.order(:name).distinct.pluck(:name)
This will work for Rails 2 (pretty old rails I know!), 3 and 4.
Client.select('distinct(name)')
This will actually use the SQL select distinct statement
SELECT distinct name FROM clients

How to make Active Record join return unique objects?

I have a simple query need: Find a list of users who made an order since Jan 1, 2013.
In SQL, it's a very simple query.
But I'm using Rails and Active Record.
So I wrote: User.joins(:orders).where("orders.created_at >= '2013-01-01 00:00:00'")
In our database, we have 100 orders made since 01/01/2013 by 75 users. (Some users made more than one order apparently.)
However, the expression above returns 100 users. (There must be duplicates.)
I tried User.joins(:orders).where("orders.created_at >= '2013-01-01 00:00:00'").uniq
That doesn't work either.
How can I get the 75 users who've made an order since 01/01/2013?
#dbjohn has the right idea, but I assume you want to avoid creating extra objects. Here's a slight variant on his solution, letting the database do the uniq-ing for you:
date = "2013-01-01 00:00:00"
User.joins(:orders).where("orders.created_at >= ?", date).distinct
Note that you can rearrange the order of methods to fit whatever you think is most semantic, and ActiveRecord will write the same SQL for you.
User.joins(:orders).
where("orders.created_at >= '2013-01-01 00:00:00'").
group('users.id')
group method will chain to the query and give you a list of unique records.
You can write nested query like this:
User.where(id: User.joins(:orders).where("orders.created_at >= '2013-01-01 00:00:00'").ids)
Rails has added uniq since version 3.2.1
so now you can use uniq
http://apidock.com/rails/ActiveRecord/QueryMethods/uniq

Resources