how to pass string value in `find_by_sql` - ruby-on-rails

Using query like:
Campaign.find_by_sql("select c.*,sc.*,org.name as org_name from campaigns as c left join super_campaigns as sc ON sc.id= c.super_campaign_id left join organisations as org ON org.id= c.organisation_id where c.status=0 AND sc.status='active'")
Getting error after using sc.status='active'.Thanks.

You can achieve that by using interpolation, here is an example from a project i made for doing something similar
self.find_by_sql("SELECT s.subject_title AS subject_name,s.subject_code AS subject_code,
COUNT(*) AS total_complaints,
COUNT(CASE p.priority_name WHEN '#{Ticket::HIGH}' THEN 1 END) AS high_complaints,
COUNT(CASE p.priority_name WHEN '#{Ticket::NORMAL}' THEN 1 END) AS normal_complaints,
COUNT(CASE p.priority_name WHEN '#{Ticket::LOW}' THEN 1 END) AS low_complaints
FROM
tickets AS t
JOIN
subjects AS s
ON t.subject_id = s.id
JOIN
priorities AS p
ON t.priority_id = p.id
WHERE
p.priority_name IN ('#{Ticket::HIGH}', '#{Ticket::NORMAL}', '#{Ticket::LOW}')
GROUP BY s.subject_title, s.subject_code
ORDER BY total_complaints ASC")
As you can see #{Ticket::HIGH} is coming from PRIORITY = [HIGH = 'high', NORMAL = 'normal', LOW = 'low'] same goes for the others
Note: this is a part of the original code.

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 joins - Return only exact matches

I have three models, Outfit, Product, and a join model OutfitProduct (Outfit has many Products through OutfitProducts).
I would like to find outfits that contain only exact product matches.
So far I have this
def by_exact_products(products)
joins(outfit_products: :product)
.where(outfit_products: { product: products })
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
end
The above returns any outfit that contains the products I am searching for, even if it is not an exact match. I would like it to return only outfits that are an exact match.
Example:
Assume we have the following outfits made up of the following products:
outfit_1.products = [product_1, product_2, product_3, product_4]
outfit_2.products = [product_1, product_2]
outfit_3.products = [product_1, product_2, product_3]
If I passed [product_1, product_2, product_3] to my query, it will return outfit_1 and outfit_3 - I would like it to only return outfit_3 which is an exact match.
UPDATE (More info)
Calling the query with an array of three products produces the following query:
SELECT "outfits".*
FROM "outfits"
INNER JOIN "outfit_products"
ON "outfit_products"."outfit_id" = "outfits"."id"
INNER JOIN "products"
ON "products"."id" = "outfit_products"."product_id"
WHERE "outfit_products"."product_id" IN ( 18337, 6089, 6224 )
GROUP BY outfits.id
HAVING ( Count(outfits.id) = 3 )
Let's first have a look at why this is happening. You use the following scenario:
outfit_1.products = [product_1, product_2, product_3, product_4]
outfit_2.products = [product_1, product_2]
outfit_3.products = [product_1, product_2, product_3]
This would have the following outfit_products table:
outfit_id | product_id
----------|-----------
1 | 1
1 | 2
1 | 3
1 | 4
2 | 1
2 | 2
3 | 1
3 | 2
3 | 3
When you add the restriction:
WHERE "outfit_products"."product_id" IN ( 1, 2, 3 )
It will eliminate the row:
outfit_id | product_id
----------|-----------
1 | 4
And leave product 1 with 3 records, when you group and count the records you'll end up with a resulting value of 3 for product 1. This means the current query will only check for a minimum of the provided products (aka make sure all the provided products are present).
To also eliminate records that have more products than the provided products you'll have to add a second count. Which counts the products without the above restriction.
def by_exact_products(products)
# all outfits that have at least all products
with_all_products = joins(outfit_products: :product)
.where(outfit_products: { product: products })
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
# all outfits that have exactly all products
joins(outfit_products: :product)
.where(id: with_all_products.select(:id))
.group("outfits.id")
.having('count(outfits.id) = ?', products.size)
end
This will select all outfits that have have at least all provided products, and count their product total.

Scope Order by Count with Conditions Rails

I have a model Category that has_many Pendencies. I would like to create a scope that order the categories by the amount of Pendencies that has active = true without excluding active = false.
What I have so far is:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).order('COUNT(pendencies.id) DESC')}
This will order it by number of pendencies, but I want to order by pendencies that has active = true.
Another try was:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).where('pendencies.active = ?', true).order('COUNT(pendencies.id) DESC')}
This will order by number of pendencies that has pendencies.active = true, but will exclude the pendencies.active = false.
Thank you for your help.
I guess you want to sort by the amount of active pendencies without ignoring categories that have no active pendencies.
That would be something like:
scope :order_by_pendencies, -> {
active_count_q = Pendency.
group(:category_id).
where(active: true).
select(:category_id, "COUNT(*) AS count")
joins("LEFT JOIN (#{active_count_q.to_sql}) AS ac ON ac.category_id = id").
order("ac.count DESC")
}
The equivalent SQL query:
SELECT *, ac.count
FROM categories
LEFT JOIN (
SELECT category_id, COUNT(*) AS count
FROM pendencies
GROUP BY category_id
WHERE active = true
) AS ac ON ac.category_id = id
ORDER BY ac.count DESC
Note that if there are no active pendencies for a category, the count will be null and will be added to the end of the list.
A similar subquery could be added to sort additionally by the total amount of pendencies...
C# answer as requested:
method() {
....OrderBy((category) => category.Count(pendencies.Where((pendency) => pendency.Active))
}
Or in straight SQL:
SELECT category.id, ..., ActivePendnecies
FROM (SELECT category.id, ..., count(pendency) ActivePendnecies
FROM category
LEFT JOIN pendency ON category.id = pendency.id AND pendnecy.Active = 1
GROUP BY category.id, ...) P
ORDER BY ActivePendnecies;
We have to output ActivePendnecies in SQL even if the code will throw it out because otherwise the optimizer is within its rights to throw out the ORDER BY.
For now I developed the following (it's working, but I believe that it's not the best way):
scope :order_by_pendencies, -> { scoped = Category.left_joins(:pendencies)
.group(:id)
.order('COUNT(pendencies.id) DESC')
.where('pendencies.active = ?', true)
all = Category.all
(scoped + all).uniq}

How to left outer joins with conditions

I have this relation:
class Action < ApplicationRecord
has_many :actions_users
I tried to make a query like:
select *
from actions left outer join actions_users
on actions_users.action_id = actions.id and actions_users.user_id = 1
where actions.user_id = 1
Meanwhile, in my experience, in all of the result that I tried,
select *
from actions left outer join actions_users
on actions_users.action_id = actions.id
where actions.user_id = 1 and actions_users.user_id = 1
the join condition code and general condition are in where function.
How can I work it out?
You can pass a string in join query and use the rails table naming conventions for this.
Action.joins("left outer join action_users on (action_users.id = actions.id and action_users.id = 1")).where('action_users.user_id = ? ', 1)
Because you have a general where condition, you can use includes. This will generate a LEFT OUTER JOIN query:
Action.includes(:actions_users).where(actions_users: { user_id: true })
Or if you are using Rails 5+, Active Record provides a finder method left_outer_joins:
Action.left_outer_joins(:actions_users).where(actions_users: { user_id: true })
Action.left_outer_joins(:actions_users).where(user_id: 1)
select *
from actions left outer join actions_users
on actions_users.action_id = actions.id and actions_users.user_id = 1
where actions.user_id = 1
Although you did not ask for it yet, ...
Action.left_outer_joins(:actions_users).where(actions_users: {status: 'active'})
select *
from actions left outer join actions_users
on actions_users.action_id = actions.id and actions_users.user_id = 1
where actions_users.status = 'active'
Up to Rails 7 there is no way to specify conditions directly on an OUTER JOIN, see the documentation for Specifying Conditions on the Joined Tables. The examples shown are suitable for INNER JOINs (as they use .where), but won't work for OUTER JOINs for the same reason.
You could try to specify the OUTER JOIN manually, but will run into problems passing parameters:
Action.joins("LEFT OUTER JOIN action_users ON (action_users.id = actions.id AND action_users.id = :user_id")
So you will need to do parameter substitution somewhat like this:
outer_join_sanitized = ApplicationRecord.sanitize_sql([
"LEFT OUTER JOIN action_users ON (action_users.id = actions.id AND action_users.id = :user_id)",
{ user_id: 22 }
])
And you could then use Actions.joins(outer_join_sanitized). At this point you might agree that just running with raw SQL from the start is the easier way to go.

Why I cannot use both ORDER BY and DISTINCT * in SQL?

I'm trying to do the following, and if I were to uncomment the distinct it will break. Also if I comment out the order and leave the distinct in, it will work.
Contestant.joins('INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = '+ season_number.to_s)
.joins('LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = '+self.id.to_s+') AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id')
.joins('INNER JOIN season_rosters ON season_rosters.season_id = V.season_id')
.where('V.is_jury_vote = (?) AND V.contestant_id <> (?) AND XV.tribal_council_key IS NOT NULL', :false, self.id)
.order('season_rosters.finished')
#.distinct
The error I get is below...
TinyTds::Error: Incorrect syntax near '*'.: EXEC sp_executesql N'SELECT DISTINCT *, __order FROM ( SELECT [contestants].*, DENSE_RANK() OVER (ORDER BY season_rosters.finished ASC) AS __order, ROW_NUMBER() OVER (PARTITION BY [contestants].* ORDER BY season_rosters.finished ASC) AS __joined_row_num FROM [contestants] INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = 6 LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = 112) AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id INNER JOIN season_rosters ON season_rosters.season_id = V.season_id WHERE (V.is_jury_vote = (''false'') AND V.contestant_id <> (112) AND XV.tribal_council_key IS NOT NULL) ) AS __sq WHERE __joined_row_num = 1 ORDER BY __order'
The issue is with this part:
SELECT DISTINCT *, __order
Try adding the required columns to your GROUP BY.
Contestant.joins('INNER JOIN votes AS V ON V.contestant_id = contestants.id AND V.season_id = '+ season_number.to_s)
.joins('LEFT OUTER JOIN votes AS XV ON (XV.contestant_id = '+self.id.to_s+') AND (XV.tribal_council_key = V.tribal_council_key) AND XV.contestant_voted_for_id = V.contestant_voted_for_id')
.joins('INNER JOIN season_rosters ON season_rosters.season_id = V.season_id')
.where('V.is_jury_vote = (?) AND V.contestant_id <> (?) AND XV.tribal_council_key IS NOT NULL', :false, self.id)
.order('season_rosters.finished')
.group('col1,col2,__order')
Also in your SQL error, order by is on a different column while in your code, it is on season_rosters.finished.

Resources