Rails 3: Filtering merit points by category in leaderboard - ruby-on-rails

The real tactical question I am facing is all categories are set as 'default' therefore if I make options[:category] = 'default' it only adds the points that have no category. Therefore if i add points to cateogry 'arin' it will not be counted to the 'default' total. So I tried to grab all tables if NOT NULL or by category but it keeps grabbing the same amount for 'arin'.
default: 20
arin: 20
Should be total of 40 if category not supplied or at 'default', if params category 'arin' then it should be 20.
Can someone help me understand the concept behind the correct SQL to get the results I am looking for?
New to rails and SQL.
def self.top_scored(options = {})
options[:table_name] ||= :users
options[:since_date] ||= 4.months.ago
options[:end_date] ||= 1.month.from_now
options[:category] ||= nil
options[:limit] ||= 10
alias_id_column = "#{options[:table_name].to_s.singularize}_id"
if options[:table_name] == :sashes
sash_id_column = "#{options[:table_name]}.id"
else
sash_id_column = "#{options[:table_name]}.sash_id"
end
# MeritableModel - Sash -< Scores -< ScorePoints
sql_query = <<SQL
SELECT
#{options[:table_name]}.id AS #{alias_id_column},
SUM(num_points) as sum_points
FROM #{options[:table_name]}
LEFT JOIN merit_scores ON merit_scores.sash_id = #{sash_id_column}
LEFT JOIN merit_score_points ON merit_score_points.score_id = merit_scores.id
WHERE merit_score_points.created_at > '#{options[:since_date]}' AND merit_score_points.created_at < '#{options[:end_date]}' AND (merit_scores.category IS NOT NULL OR merit_scores.category = '#{options[:category]}')
GROUP BY #{options[:table_name]}.id, merit_scores.sash_id
ORDER BY sum_points DESC
LIMIT #{options[:limit]}
SQL
results = ActiveRecord::Base.connection.execute(sql_query)
results.map do |h|
h.keep_if { |k, v| (k == alias_id_column) || (k == 'sum_points') }
end
results
end
end

Seems no one answered and only down voted. Here is to anyone that questions this in the future. I figured out you can split sql statements and use an if statement in rails around the SQL.
sql_query = "SELECT
#{options[:table_name]}.id AS #{alias_id_column},
SUM(num_points) as sum_points
FROM #{options[:table_name]}
LEFT JOIN merit_scores ON merit_scores.sash_id = #{sash_id_column}
LEFT JOIN merit_score_points ON merit_score_points.score_id = merit_scores.id
WHERE merit_score_points.created_at > '#{options[:since_date]}' AND merit_score_points.created_at < '#{options[:end_date]}' "
if(options[:category] != nil)
sql_query += "AND merit_scores.category = \"#{options[:category]}\" "
end
sql_query += "GROUP BY #{options[:table_name]}.id, merit_scores.sash_id
ORDER BY sum_points DESC
LIMIT #{options[:limit]} "
results = ActiveRecord::Base.connection.execute(sql_query)

Related

Arel: I get "stack level too deep" error when I try to get SQL from Arel

First I'd like to describe idea of what I am trying to do. I have "jobstat_jobs" table where I store the information about computing job perfomance. I am trying to compose 2 queries: 1) jobs grouped by project 2) jobs grouped by project and state. Then these queries are inner joined and I want to display share of jobs of each state among all jobs. I implemented it using ActiveRecord and raw sql, but I can't do it with arel. I get the "stack level too deep" on the "joined.to_sql" line.
members = Core::Member.arel_table
jobs = Perf::Job.arel_table
cool_relation = jobs.where(jobs[:state].not_in(%w[COMPLETETED RUNNING unknown]))
relation = cool_relation.join( Arel::Nodes::SqlLiteral.new <<-SQL
INNER JOIN core_members ON core_members.login = jobstat_jobs.login
SQL
).join(Arel::Nodes::SqlLiteral.new <<-SQL
RIGHT JOIN sessions_projects_in_sessions ON
sessions_projects_in_sessions.project_id = core_members.project_id
SQL
).group(members[:project_id]).project(members[:project_id].as('id'))
hours = '(extract(epoch from (end_time - start_time))/ 3600)'
selections = {
node_hours: "(sum((#{hours})*num_nodes))",
jobs: "count(jobstat_jobs.id)"
}
selections.each do |key, value|
relation = relation.project(
Arel::Nodes::SqlLiteral.new(value).as(key.to_s)
)
end
state_relation = relation.project(jobs[:state].as('state'))
.group(jobs[:state])
s = state_relation.as('s')
pp ActiveRecord::Base.connection.exec_query(state_relation.to_sql).to_a
joined = relation.join(s)
.on(jobs[:id].eq(s[:id]))
.project(s[:id], s[:state])
puts joined.to_sql
joined
I noticed the strange thing. When I replace "joined = relation" with "jobs.where(jobs[:state].not_in(%w[COMPLETETED RUNNING unknown]))" it works. But when I replace "joined = relation" with "joined = cool_relation" it doesn't work and I get "stack level too deep" (these 2 replacements are almost the same).
Arel v 9.0.0, Postgresql
My problem is that I expected arel to create a new object each time I chain method(like ActiveRecord::Relation).
Just add #clone method here:
joined = relation.clone.join(s)
.on(jobs[:id].eq(s[:id]))
.project(s[:id], s[:state])
I got SQL string, but it was wrong and there were exceptions on database level. Now my code is following:
members = Core::Member.arel_table
jobs = Perf::Job.arel_table
cool_relation = jobs.where(jobs[:state].not_in(%w[COMPLETETED RUNNING unknown]))
relation = cool_relation.join( Arel::Nodes::SqlLiteral.new <<-SQL
INNER JOIN core_members ON core_members.login = jobstat_jobs.login
SQL
.gsub("\n", ' ')).join(Arel::Nodes::SqlLiteral.new <<-SQL
RIGHT JOIN sessions_projects_in_sessions ON
sessions_projects_in_sessions.project_id = core_members.project_id
SQL
.gsub("\n", ' ')).group(members[:project_id]).project(members[:project_id].as('id'))
hours = '(extract(epoch from (end_time - start_time))/ 3600)'
selections = {
node_hours: "(sum((#{hours})*num_nodes))",
jobs: "count(jobstat_jobs.id)"
}
selections.each do |key, value|
relation = relation.project(
# Arel::Nodes::SqlLiteral.new(value).as(key.to_s)
Arel::Nodes::SqlLiteral.new("(CAST(#{value} AS decimal))").as(key.to_s)
)
end
state_relation = relation.clone.project(jobs[:state].as('state'))
.group(jobs[:state])
s = state_relation.as('s')
n = relation.as('n')
pp ActiveRecord::Base.connection.exec_query(state_relation.to_sql).to_a
pp ActiveRecord::Base.connection.exec_query(relation.to_sql).to_a
manager = Arel::SelectManager.new
joined = manager.project(s[:id], s[:state])
.from(s)
.join(n).on(s[:id].eq(n[:id]))
selections.keys.each do |key|
joined = joined.project(s[key].as("s_#{key}"), n[key].as("n_#{key}"))
.project(s[key] / n[key].as("share_#{key}"))
end
puts joined.to_sql
joined
Pay attention to the #clone method used here too. When I remove #clone, project method affects relation variable too and I get wrong SQL because of that.
The joined.to_sql line produces the following and works as expected:
SELECT s."id", s."state", s."node_hours" AS s_node_hours,
n."node_hours" AS n_node_hours, s."node_hours" / n."node_hours" AS
share_node_hours, s."jobs" AS s_jobs, n."jobs" AS n_jobs,
s."jobs" / n."jobs" AS share_jobs FROM (SELECT "core_members".
"project_id" AS id, (CAST((sum(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes)) AS decimal)) AS node_hours,
(CAST(count(jobstat_jobs.id) AS decimal)) AS jobs,
"jobstat_jobs"."state" AS state FROM "jobstat_jobs" INNER JOIN
core_members ON core_members.login = jobstat_jobs.login
RIGHT JOIN sessions_projects_in_sessions ON sessions_projects_in_sessions.project_id = core_members.project_id
WHERE "jobstat_jobs"."state" NOT IN ('COMPLETETED', 'RUNNING', 'unknown')
GROUP BY "core_members"."project_id", "jobstat_jobs"."state") s INNER JOIN
(SELECT "core_members"."project_id" AS id, (CAST((sum(((extract(epoch from
(end_time - start_time))/ 3600))*num_nodes)) AS decimal)) AS node_hours,
(CAST(count(jobstat_jobs.id) AS decimal)) AS jobs FROM "jobstat_jobs"
INNER JOIN core_members ON core_members.login = jobstat_jobs.login RIGHT JOIN sessions_projects_in_sessions ON
sessions_projects_in_sessions.project_id = core_members.project_id WHERE
"jobstat_jobs"."state" NOT IN ('COMPLETETED', 'RUNNING', 'unknown') GROUP BY
"core_members"."project_id") n ON s."id" = n."id"
Now that I understand the desired output here is how I would go about this
class Report
JOB_STATS = Arel::Table.new('jobstat_jobs')
CORE_MEMBERS = Arel::Table.new('core_members')
SESSIONS = Arel::Table.new('sessions_projects_in_sessions')
def additions
# This could be ported too if I knew the tables for end_time, start_time, and num_nodes
{
node_hours: Arel.sql("((extract(epoch from (end_time - start_time))/ 3600))*num_nodes").sum,
jobs: JOB_STATS[:id].count
}
end
def n
#n ||= _base_query.as('n')
end
def s
#s ||= _base_query
.project(JOB_STATS[:state])
.group(JOB_STATS[:state]).as('s')
end
def alias_columns
additions.keys.flat_map do |key|
[s[key].as("s_#{key}"),
n[key].as("n_#{key}"),
(s[key] / n[key]).as("share_#{key}")]
end
end
def query
Arel::SelectManager.new.project(
s[:project_id].as('id'),
s[:state],
*alias_columns
)
.from(s)
.join(n).on(s[:project_id].eq(n[:project_id]))
end
def to_sql
query.to_sql
end
private
def cast_as_decimal(value,alias_name:)
Arel::Nodes::NamedFunction.new(
"CAST",
[Arel::Nodes::As.new(value, Arel.sql('DECIMAL'))]
).as(alias_name.to_s)
end
def _base_query
JOB_STATS
.project(
CORE_MEMBERS[:project_id],
*additions.map {|k,v| cast_as_decimal(v, alias_name: k)})
.join(CORE_MEMBERS).on(CORE_MEMBERS[:login].eq(JOB_STATS[:login]))
.outer_join(SESSIONS).on(SESSIONS[:project_id].eq(CORE_MEMBERS[:project_id]))
.where(JOB_STATS[:state].not_in(['COMPLETETED', 'RUNNING', 'unknown']))
.group(CORE_MEMBERS[:project_id])
end
end
Result of Report.new.to_sql
SELECT
s."project_id" AS id,
s."state",
s."node_hours" AS s_node_hours,
n."node_hours" AS n_node_hours,
s."node_hours" / n."node_hours" AS share_node_hours,
s."jobs" AS s_jobs,
n."jobs" AS n_jobs,
s."jobs" / n."jobs" AS share_jobs
FROM
(
SELECT
"core_members"."project_id",
CAST(SUM(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes) AS DECIMAL) AS node_hours,
CAST(COUNT("jobstat_jobs"."id") AS DECIMAL) AS jobs,
"jobstat_jobs"."state"
FROM
"jobstat_jobs"
INNER JOIN "core_members" ON "core_members"."login" = "jobstat_jobs"."login"
LEFT OUTER JOIN "sessions_projects_in_sessions" ON "sessions_projects_in_sessions"."project_id" = "core_members"."project_id"
WHERE
"jobstat_jobs"."state" NOT IN (N'COMPLETETED', N'RUNNING', N'unknown')
GROUP BY
"core_members"."project_id",
"jobstat_jobs"."state"
) s
INNER JOIN (
SELECT
"core_members"."project_id",
CAST(SUM(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes) AS DECIMAL) AS node_hours,
CAST(COUNT("jobstat_jobs"."id") AS DECIMAL) AS jobs
FROM
"jobstat_jobs"
INNER JOIN "core_members" ON "core_members"."login" = "jobstat_jobs"."login"
LEFT OUTER JOIN "sessions_projects_in_sessions" ON "sessions_projects_in_sessions"."project_id" = "core_members"."project_id"
WHERE
"jobstat_jobs"."state" NOT IN (N'COMPLETETED', N'RUNNING', N'unknown')
GROUP BY
"core_members"."project_id"
) n ON s."project_id" = n."project_id"
This will also allow you further filter the resulting query like so:
rpt = Report.new
q = rpt.query.where(rpt.n[:jobs].gt(12))
q.to_sql
#=> "...same as above...WHERE n.\"jobs\" > 12"

Activerecord query where current employer is X and previous employer is Y

Basically I'd like to return all people whose current job title is X and whose previous job title is Y. As an example, I have a talent whose current emnployment is "Airbnb (company_id = 1)" and whose previous employment is at "Youtube (company_id = 2)".
If I run a query to find talent where current employment is Airbnb:
Talent.joins(:job_histories).where(["job_histories.company_id = ? and job_histories.end_year = ?", 1, "Present"])
I get the person.
If I run a query where previous employment is Youtube (hence the end_year != "Present" below)
Talent.joins(:job_histories).where(["job_histories.company_id = ? and job_histories.end_year != ?", 2, "Present"])
I also get the same person.
However, if I chain them together to find talents where current employer is Airbnb AND previous employer is Youtube, like this:
#talents = Talent.all
#talents = #talents.joins(:job_histories).where(["job_histories.company_id = ? and job_histories.end_year = ?", 1, "Present"])
#talents = #talents.joins(:job_histories).where(["job_histories.company_id = ? and job_histories.end_year != ?", 2, "Present"])
I do not get any results. I've tried several variations of the query but none return anything.
The only way I can get it to work is by using the first query and then looping over each talent to find where job_histories.company_id == 2.
if params[:advanced_current_company] && params[:advanced_previous_company]
#talents = #talents.joins(:job_histories).where(job_histories: { company_id: params[:advanced_current_company] }).distinct if params[:advanced_current_company]
#talents.each do |talent|
talent.job_histories.each do |job_history|
if job_history.company_id == params[:advanced_previous_company][0].to_i
new_talents.append(talent.id)
end
end
end
#talents = Talent.where(id: new_talents)
end
Any direction would be amazing. Thanks!
You had the right idea with a double join of the job_histories, but you need to alias the job_histories table names to be able to differentiate between them in the query, as otherwise activerecord will think it's only one join that needs to be done.
Talent.joins("INNER JOIN job_histories as jh1 ON jh1.talent_id = talents.id")
.joins("INNER JOIN job_histories as jh2 ON jh2.talent_id = talents.id")
.where("jh1.company_id = ? and jh1.end_year = ?", 1, "Present")
.where("jh2.company_id = ? and jh2.end_year != ?", 2, "Present")

How to loop through arrays of different length in Ruby?

Let's say i have two relation arrays of a user's daily buy and sell.
how do i iterate through both of them using .each and still let the the longer array run independently once the shorter one is exhaused. Below i want to find the ratio of someone's daily buys and sells. But can't get the ratio because it's always 1 as i'm iterating through the longer array once for each item of the shorter array.
users = User.all
ratios = Hash.new
users.each do |user|
if user.buys.count > 0 && user.sells.count > 0
ratios[user.name] = Hash.new
buy_array = []
sell_array = []
date = ""
daily_buy = user.buys.group_by(&:created_at)
daily_sell = user.sells.group_by(&:created_at)
daily_buy.each do |buy|
daily_sell.each do |sell|
if buy[0].to_date == sell[0].to_date
date = buy[0].to_date
buy_array << buy[1]
sell_array << sell[1]
end
end
end
ratio_hash[user.name][date] = (buy_array.length.round(2)/sell_array.length)
end
end
Thanks!
You could concat both arrays and get rid of duplicated elements by doing:
(a_array + b_array).uniq.each do |num|
# code goes here
end
Uniq method API
daily_buy = user.buys.group_by(&:created_at)
daily_sell = user.sells.group_by(&:created_at
buys_and_sells = daily_buy + daily_sell
totals = buys_and_sells.inject({}) do |hsh, transaction|
hsh['buys'] ||= 0;
hsh['sells'] ||= 0;
hsh['buys'] += 1 if transaction.is_a?(Buy)
hsh['sells'] += 1 if transaction.is_a?(Sell)
hsh
end
hsh['buys']/hsh['sells']
I think the above might do it...rather than collecting each thing in to separate arrays, concat them together, then run through each item in the combined array, increasing the count in the appropriate key of the hash returned by the inject.
In this case you can't loop them with each use for loop
this code will give you a hint
ar = [1,2,3,4,5]
br = [1,2,3]
array_l = (ar.length > br.length) ? ar.length : br.length
for i in 0..array_l
if ar[i] and br[i]
puts ar[i].to_s + " " + br[i].to_s
elsif ar[i]
puts ar[i].to_s
elsif br[i]
puts br[i].to_s
end
end

Rails: Search for person with language skills - e,g, speaks "German AND English" on one-to-many table

This must be a basic thing in rails, but I don't know how to do it.
I would like to filter participants based on the languages they speak. People can speak multiple languages, and languages are stored in their own table with a one-to-many relationship.
Now my search looks really clunky and doesn't seem to work:
if #cvsearch.language.present? == true and #cvsearch.language != 0
#p = #p.joins(:languages).where('languages.name = ?', #cvsearch.language)
else
#cvsearch.language = 0
end
if #cvsearch.language1.present? == true and #cvsearch.language1 != 0
#p = #p.joins(:languages).where('languages.name = ?', #cvsearch.language1)
end
if #cvsearch.language2.present? == true and #cvsearch.language2 != 0
#p = #p.joins(:languages).where('languages.name = ?', #cvsearch.language2)
end
if #cvsearch.language3.present? == true and #cvsearch.language3 != 0
#p = #p.joins(:languages).where('languages.name = ?', #cvsearch.language3)
end
The resulting SQL, slightly shortened:
SELECT COUNT(*) FROM "participants" INNER JOIN "languages" ON "languages"."participant_id" = "participants"."id" WHERE (participants.id >= 2) AND (languages.name = 11) AND (languages.name = 10)[0m
It would be great to get a specific solution, but even better is a pointer as to where I can read up on this - what's the key word I am missing to describe this problem?
So this is the solution I am using for now:
if #cvsearch.language1.present? == true and #cvsearch.language1 != 0
safe_lang = ActiveRecord::Base::sanitize(#cvsearch.language1)
qry = "INNER JOIN languages l1 ON l1.participant_id = participants.id AND l1.name = " + safe_lang.to_s
#p = #p.joins(qry)
end
Works wonderfully, just need to get some feedback regarding the safety of this approach.
I'm not sure of a general reference to refer you to, but this is basic SQL stuff. Basically, the JOIN is performed first resulting in a number of rows and then the WHERE is applied, filtering the rows. The conceptual mistake here is thinking that the WHERE clause will somehow apply to the full set of matched languages, but it doesn't work that way, each row of the result is considered in isolation, therefore a clause like (languages.name = 11) AND (languages.name = 10) will never return anything, because languages.name only has a single value in each row. The query as constructed could only work for an OR clause, so you could say something like WHERE (languages.name = 11) OR (languages.name = 12).
In order to filter down the participants you need one join for each language, so you want something like this:
SELECT COUNT(*) FROM participants
INNER JOIN languages l1 ON l1.participant_id = participants.id AND (languages.name = 10)
INNER JOIN languages l2 ON l2.participant_id = participants.id AND (languages.name = 11)
WHERE participants.id >= 2
Offhand I'm not sure of the easiest way to do this in ActiveRecord, it's not a super common query. Your general structure should work, but with something like:
if #cvsearch.language1.present? == true and #cvsearch.language1 != 0
safe_language = ActiveRecord::Base.sanitize(#cvssearch.language1)
join_clause = "INNER JOIN languages l1 ON l1.participant_id = participants.id AND language.name = #{safe_language}"
#p = #p.joins(join_clause)
end

in ruby how do I add objects to an array, keep a counter for every object and still check easily if the object is in the array?

I want to compile a list of recommended friends.
What I was thinking was something like this (this is semi pseudo (sudo) code!):
recommended_friends = []
friends.each do |friend|
while recommeded_friends.length < 10
friend.friends.each do |friend|
if friend.in?(recommeded_friends)
recommeded_friends[friend][counter] += 1
else
recommeded_friends << [friend, 0]
end
end
end
end
But this obviously doesn't work. How would you guys approach this?
Thanks for any suggestions.
The tables (some are shortened):
Users:
id | name
Friendships
id | user_1_id | user_2_id | requested_at | accepted_at | declined_at |
A friendship between user1 and user2 only occurs once in the DB.
UPDATED.
Try something like this, it should work:
recommended_friends = {}
friends.each do |friend|
if recommeded_friends.length < 10
friend.friends.each do |other_friend|
if other_friend != this_user # exclude myself
recommeded_friends[other_friend] =
(recommeded_friends[other_friend] | 0) + 1
end
end
end
end
recommendend_friends.sort_by{|key, value| value}.reverse
top_ten = recommended_friends.first(10).map{|a| a[0]}
SQL version:
Users.find_by_sql([
"SELECT u.*
FROM
(SELECT f2.id, f2.user_1_id u_1_id, f2.user_2_id u_2_id, (count(f1.id)) cnt
FROM friendships f1
JOIN friendships f2 ON f1.user_1_id = f2.user_1_id
OR f1.user_2_id = f2.user_1_id
OR f1.user_2_id = f2.user_2_id
OR f1.user_1_id = f2.user_2_id
WHERE (f1.user_1_id = ? OR f1.user_2_id = ?)
AND (f2.user_1_id <> ? AND f2.user_2_id <> ?)
GROUP BY f2.id, f2.user_1_id, f.user_2_id
HAVING count(f2.id) = 1
ORDER BY cnt DESC) fs
JOIN friendships ff ON ff.user_1_id = fs.u_1_id
OR ff.user_2_id = fs.u_1_id
OR ff.user_2_id = fs.u_2_id
OR ff.user_1_id = fs.u_2_id
JOIN users u ON
CASE WHEN (ff.user_1_id = fs.u_1_id OR ff.user_2_id = fs.u_1_id)
THEN fs.u_2_id ELSE fs.u_1_id END = u.id ",
user.id, user.id, user.id, user.id]).first(10)
In theory it should work, take a try.
recommeded_friends will always stay an empty array.
You can not do this: recommeded_friends < 10
Try this: recommeded_friends.length < 10
The simplest method I can think of:
recommended_friends = friend.friends.sort do |a,b|
a <=> b # insert your ranking algorithm here
end.take 10

Resources