Sum active record with expression rails - ruby-on-rails

I have this code in my sql view:
SELECT
reports.`date`
reports.book_title
SUM( reports.net_sold_count * reports.avg_list_price / 10 ) AS revenue
FROM
reports
GROUP BY
reports.date,
reports.book_title
I need to translate it to ruby code
I have active record model
class Report < ActiveRecord::Base
Right now I have this code:
Report.group(:date, :book_title)
But I don't know where to go next because .sum in all examples accepts only one argument.(http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-sum)
UPD:
Right now, I am thinking of trying something like
Report.select('date, book_title, SUM( reports.net_sold_count * reports.avg_list_price / 10 ) AS revenue').group(:date, :book_title)

The #sum documentation is misleading. Yes, #sum only accepts one argument but that argument doesn't have to be a symbol corresponding to a column name, that argument can also be an SQL expression that uses the available columns. In particular, the argument to #sum could be the Ruby string:
'reports.net_sold_count * reports.avg_list_price / 10'
and so you could say:
Report.group(:date, :book_title).sum('reports.net_sold_count * reports.avg_list_price / 10')
That will give you a Hash that looks like:
{
[some_date, some_title] => some_sum,
...
}
that you can pull apart as needed back in Ruby land.

Related

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! :-)

update_all with a method

Lets say I have a model:
class Result < ActiveRecord::Base
attr_accessible :x, :y, :sum
end
Instead of doing
Result.all.find_each do |s|
s.sum = compute_sum(s.x, s.y)
s.save
end
assuming compute_sum is a available method and does some computation that cannot be translated into SQL.
def compute_sum(x,y)
sum_table[x][y]
end
Is there a way to use update_all, probably something like:
Result.all.update_all(sum: compute_sum(:x, :y))
I have more than 80,000 records to update. Each record in find_each creates its own BEGIN and COMMIT queries, and each record is updated individually.
Or is there any other faster way to do this?
If the compute_sum function can't be translated into sql, then you cannot do update_all on all records at once. You will need to iterate over the individual instances. However, you could speed it up if there are a lot of repeated sets of values in the columns, by only doing the calculation once per set of inputs, and then doing one mass-update per calculation. eg
Result.all.group_by{|result| [result.x, result.y]}.each do |inputs, results|
sum = compute_sum(*inputs)
Result.update_all('sum = #{sum}', "id in (#{results.map(&:id).join(',')})")
end
You can replace result.x, result.y with the actual inputs to the compute_sum function.
EDIT - forgot to put the square brackets around result.x, result.y in the group_by block.
update_all makes an sql query, so any processing you do on the values needs to be in sql. So, you'll need to find the sql function, in whichever DBMS you're using, to add two numbers together. In Postgres, for example, i believe you would do
Sum.update_all(sum: "x + y")
which will generate this sql:
update sums set sum = x + y;
which will calculate the x + y value for each row, and set the sum field to the result.
EDIT - for MariaDB. I've never used this, but a quick google suggests that the sql would be
update sums set sum = sum(x + y);
Try this first, in your sql console, for a single record. If it works, then you can do
Sum.update_all(sum: "sum(x + y)")
in Rails.
EDIT2: there's a lot of things called sum here which is making the example quite confusing. Here's a more generic example.
set col_c to the result of adding col_a and col_b together, in class Foo:
Foo.update_all(col_c: "sum(col_a + col_b)")
I just noticed that i'd copied the (incorrect) Sum.all.update_all from your question. It should just be Sum.update_all - i've updated my answer.
I'm completely beginner, just wondering Why not add a self block like below, without adding separate column in db, you still can access Sum.sum from outside.
def self.sum
x+y
end

Rails/Postgres treat string column as integer

I got a table named companies and a column named employees, which is a string column.
My where condition to find companies which have between 10 and 100 employees:
where("companies.employees >= ? AND companies.employees <= ?", 10, 100)
The problem is: The column needs to remain a string column so I can't just convert it to integer but I also want to compare the employee numbers. Is there any way to do this?
This may work, it is a ruby question, I don't know ruby :-) In postgres I would write the query as Craig says, like this:
select * from companies where employees::integer >= 10 and employees::integer <= 100;
(Of course there is substitution, etc, but this gets the concept across. One of the problems you run in to when you don't use the correct type in postgres is that indices don't work right. Since you are casting the employees to an integer type, you have to fetch every record, convert it to an integer, then filter using the greater/less than stuff. Every record in the table will be fetched, casted, then compared. If this was an integer type to start with, and there was an index on the table, then the postgres engine can do a lot better performance wise by selecting only the relevant records. Anyway...
Your ruby may work modified like this:
where("companies.employees::integer >= ? AND companies.employees::integer <= ?", 10, 100)
But, that makes me curious about the substitution. If the type is gleaned from the type of the argument, then it might work because the 10 and 100 are clearly integers. If the substitution gets weird, you might be able to do this:
where("companies.employees::integer >= cast(? as integer) AND companies.employees::integer <= cast(? as integer)", 10, 100)
You can use that syntax for the entire query as well:
where("cast(companies.employees as integer) >= cast(? as integer) AND cast(companies.employees as integer) <= cast(? as integer)", 10, 100)
One of these variants might work. Good Luck.
-g

How do I put a Ruby function into a SQLite3 query?

I have a function which I need to put into a SQLite3 query.
I have the following method:
def levenshtein(a, b)
case
when a.empty? then b.length
when b.empty? then a.length
else [(a[0] == b[0] ? 0 : 1) + levenshtein(a[1..-1], b[1..-1]),
1 + levenshtein(a[1..-1], b),
1 + levenshtein(a, b[1..-1])].min
end
end
and I want to do a query that looks something like this:
#results = The_db.where('levenshtein("name", ?) < 3', '#{userinput}')
What I want to find the values of name in The_db where the edit distance between the value of the name column and the user input is less than 3. The problem is that I don't know how to use a Ruby function in a query. Is this even possible?
Have a look at the create_function method. You can use it to create a custom function like this (where you have already defined your levenshtein Ruby method):
db.create_function "levenshtein", 2 do |func, a, b|
func.result = levenshtein(a, b)
end
You can then use it in your SQL:
# first set up some data
db.execute 'create table names (name varchar(30))'
%w{Sam Ian Matt John Albert}.each do |n|
db.execute 'insert into names values (?)', n
end
#Then use the custom function
puts db.execute('select * from names where levenshtein(name, ?) < 3', 'Jan')
In this example the output is
Sam
Ian
John
I haven’t tested this in Rails, but I think it should work, the query string is just passed through to the database. You can get the Sqlite db object using ActiveRecord::Base.connection.raw_connection. (I don’t know how to configure Rails so that all ActiveRecord connections have the function defined however – it does seem to work if you add the function in the controller, but that isn’t ideal).
Having shown how this can be done, I’m not sure if it should be done in a web app. You probably don’t want to use Sqlite in production. Perhaps it could be useful if you’re using e.g. Postgres with its own levenshtein function in production (as suggested in the Tin Man’s answer), but want to use Sqlite in development.
If you need to retrieve the value of a column to insert it into a Ruby function, then you have to retrieve that value first.
The DBM can't call your method in your running code; You have to have the value, pass it to your method, then use the result in a secondary query.
Or use a DBM that has a Levenshtein function built in, like PostgreSQL or define it in pure SQL.

Column Calculations Rails 3

my app has invoices and invoice_items. Each invoice has many invoice_items.
In my invoice_items model, I have a calculation to work out the total:
def total
#total ||= quantity.to_d * price
end
That works fine. What I'm trying to do is calculate the sum of the totals and I'm stuck.
In the console, I've tried this:
invoice = Invoice.first
invoice.invoice_items.sum(:total)
But I get an error saying :total doesn't exist. Which I guess it doesn't.
My question is how I can go about doing this calculation?
-- UPDATE --
I've tried the following as per #paukul's answer:
invoice.invoice_items.sum(&:total)
This gives the error:
ArgumentError: wrong number of arguments (1 for 2)
Thanks
you are calling .sum on the whole invoice_items array (which obviously doesn't know the .total method) and not each individual invoice item.
To make it work you want to use the block version of sum which yields every single item in the array to the block and sums up their results.
with the symbol to proc syntactic sugar, you are almost there.
Try invoice.invoice_items.all.sum(&:total) which is equivalent to invoice.invoice_items.inject(0) { |sum, item| sum + item.total }
see the RDoc for Enumerable#sum

Resources