How to write SQL query as named_scope? - sql

How can I translate the following SQL query into a named_scope?
select users.*, sum(total_quantity * total_price) as points_spent
from orders
join users on users.id = orders.user_id
where pay_type = 'points'
group by user_id
order by points_spent desc
Thanks!

Try This
class User < ActiveRecord::Base
named_scope :your_name,
:select=>" users.*,sum(total_quantity * total_price) as points_spent",
:joins => :orders,
:conditions => ['pay_type = ?', 'points'],
:group ="user_id ",
:order=>'points_spent desc'
end

Related

Arel wth lateral table in the from clause,

I have 3 table users, roles and roles_users where in the last table is the intermediate table. So, user can have multiple roles and any role can belong to multiple users.
The problem statement is that we need to render the list of users and their roles matching the search criteria.
select users.id, dynamic_roles.name
from users, lateral (
select GROUP_CONCAT( DISTINCT( roles.name ) ) as name
from roles, roles_users
where (
( roles_users.user_id = users.id AND roles_users.role_id = roles.id )
)
) dynamic_roles
where dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%';
What I tried is as below:
rs = Role.joins(:users).select("GROUP_CONCAT( DISTINCT( #{Role.table_name}.name ) ) as name")
users = User.arel_table #predefined reference received as argument to a method that is supposed to compose the arel query.
users = users.project(users['id']).distinct
users.to_sql
=> "SELECT DISTINCT users.id FROM users"
users.from('dynamic_roles').to_sql
=> "SELECT DISTINCT users.id FROM dynamic_roles"
users.lateral('dynamic_roles').to_sql
=> TypeError: Cannot visit Arel::Nodes::Lateral
from /Users/prasadsurase/.rvm/gems/ruby-2.7.1/gems/activerecord-6.0.3.6/lib/arel/visitors/visitor.rb:39:in `rescue in visit'
Caused by NoMethodError: undefined method `visit_Arel_Nodes_Lateral' for #<Arel::Visitors::MySQL:0x00007f97424477b8>
Referring https://apidock.com/rails/v6.0.0/Arel/SelectManager/from and https://apidock.com/rails/v6.0.0/Arel/SelectManager/lateral
Your issue is that the mysql visitors do not include a visitor for Lateral (the way postgres does).
We could go about adding our own visitor but it is easier to get around this using a NamedFunction instead since the LATERAL syntax is the same as a function.
As with most things complicated, this is not the prettiest solution but it will provide the desired result.
roles = Arel::Table.new('roles')
role_users = Arel::Table.new('role_users')
users = Arel::Table.new('users')
group = Arel::Nodes::NamedFunction.new('GROUP_CONCAT',
[Arel::Nodes::Grouping.new(roles[:name])],
'name')
group.distinct = true
sub = roles.project(group)
.from([roles, role_users])
.where( role_users[:user_id].eq(users[:id]).and(
role_users[:role_id].eq( roles[:id]))
)
dynamic_roles = Arel::Table.new('dynamic_roles')
lateral = Arel::Nodes::NamedFunction.new('LATERAL',[sub], dynamic_roles.name)
users.project(users[:id], dynamic_roles[:name])
.from([ users, lateral])
.where( dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%admin%")).and(
dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%manager%"))
))
This will Produce the following SQL:
SELECT users.id, dynamic_roles.name
FROM users, LATERAL(
(
SELECT GROUP_CONCAT( DISTINCT (roles.name)) AS name
FROM roles, role_users
WHERE role_users.user_id = users.id AND role_users.role_id = roles.id
)
) AS dynamic_roles
WHERE
dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%'

Raw SQL statement invalid

If this query would be done in Rails ORM, it would look like
ids = User.find(user_id).conversations.pluck(:id)
UserMessage.where("'conversation'.'id' IN (?)", ids)
My models are:
class User
has_many :conversations, through: :user_conversations
end
class UserMessage
belongs_to :user
belongs_to :conversation
end
In raw SQL:
ActiveRecord::Base.connection.execute(
%( SELECT "user_messages".*,
( SELECT array_agg(t)
FROM (
SELECT "conversations"."id"
FROM "conversations"
INNER JOIN "users_conversations" ON "conversations"."id" = "users_conversations"."conversation_id"
WHERE "users_conversations"."user_id" = 'f044e064-0b6f-4371-91aa-3c03e31c8ad8'
) t
) AS this_user_conversations
FROM "user_messages"
WHERE "user_messages"."conversation_id" IN (this_user_conversations)))
this gives
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "this_user_conversations" does not exist
LINE 11: WHERE "user_messages"."conversation_id" IN (this_user_conver...
But well as far as I understand, this query defines this value with AS statement.
The where clause happens before the select list so it can't see the subquery. An alternative:
select
um.*, tuc.this_user_conversations
from
"user_messages" um
inner join
(
select array_agg(c."id") as this_user_conversations
from
"conversations" c
inner join
"users_conversations" uc on c."id" = uc."conversation_id"
where uc."user_id" = 'f044e064-0b6f-4371-91aa-3c03e31c8ad8'
) tuc on um."conversation_id" = any (tuc.this_user_conversations)

Ruby Gem Squeel, how to write self join

I'm trying to write the following query using the ruby gem Squeel
SELECT COUNT(*)
FROM(
SELECT
a.end_at AS START,
Min(b.start_at) AS END
FROM periods AS a
JOIN periods AS b ON b.season_id IN (1,2,3) AND a.end_at <= b.start_at
WHERE a.season_id IN (1,2,3)
GROUP BY a.end_at
HAVING a.end_at < MIN(b.start_at)
) AS gaps
WHERE
gaps.START < '2013-05-17' AND gaps.END > '2013-05-05';
Any idea on how to achieve this?
I can get the seasons_ids using:
seasons = self.seasons
Period.where{season_id.in(seasons.select{id})}
but the self join with condition, I have no idea how to tackle this so far.
This is similar to what I've tried to do here.
What I would try is using a sub-query, joining Period to itself as follows:
class Period < ActiveRecord::Base
attr_accessible :end_at, :season_id, :start_at
belongs_to :season
scope :in_seasons, ->(season_ids) { joins{season}.where{id.in(season_ids)} }
def self.find_gaps(start_date, end_date)
season_ids = ["1", "2", "3"]
scope = select{[end_at.as(`gap_start`), `min(self_join.start_at)`.as(`gap_end`)]}
scope = scope.joins{"LEFT JOIN (" + Period.in_seasons(season_ids).to_sql + ") AS self_join ON self_join.end_at <= periods.start_at"}
scope = scope.where{(`self_join.start_at` != nil) & (`gap_start` < start_date) & (`gap_end` > end_date)}
scope = scope.group{`gap_end`}.having{end_at < `min(self_join.start_at)`}
end
end
In rails console, Period.find_gaps('2001-01-01', '2010-01-01').to_sql produces (formatting is my own):
SELECT
\"periods\".\"end_at\" AS gap_start,
min(self_join.start_at) AS gap_end
FROM \"periods\"
LEFT JOIN
(SELECT \"periods\".*
FROM \"periods\"
INNER JOIN \"seasons\" ON \"seasons\".\"id\" = \"periods\".\"season_id\"
WHERE \"periods\".\"id\" IN (1, 2, 3)
) AS self_join ON self_join.end_at <= periods.start_at
WHERE ((self_join.start_at IS NOT NULL AND gap_start < '2001-01-01' AND gap_end > '2010-01-01'))
GROUP BY gap_end
HAVING \"periods\".\"end_at\" < min(self_join.start_at)
It looks like what you are trying to get at... at least the inner portion.
Hope it helps.

Rails, ActiveRecord, complex query with multiple joins on nested models

I've been trying to create this complex query for some time now to no avail through ActiveRecord. I'm trying to get the workouts that have the highest workout_set.weight for the specified user and exercise and order it by the weight.
Models look like so (erroneous fields removed):
Workout
belongs_to :user
has_many :workout_exercises
WorkoutExercises
belongs_to :workout
belongs_to :exercise
has_many :workout_sets
WorkoutSet
belongs_to :workout_exercise
weight
So for example, with the following data (assume, the exercise_id is the same):
Steve:
Workout 1:
weight: 500
Workout 2:
weight: 400
Mark:
Workout 1:
weight: 300
Workout 2:
weight: 350
The expected result set would be:
Steve's Workout 1
Mark's Workout 2
This is on PostgreSql, so the constraints are stricter than sqLite and MySql.
UPDATE:
Since I'm running on PostgreSql, the DB is much more strict on the order_by section of the query. Here's the RSpec test with everything written out for the intent of clarity:
it 'fetches the workout with the highest weight' do
workout = create(:workout_with_exercises, user: user)
workout2 = create(:workout_with_exercises, user: user)
workout.workout_exercises[0].workout_sets[0].weight = 200
workout.save
workout2.workout_exercises[0].workout_sets[0].weight = 100
workout2.save
expect(user.workouts.count).to eq 2
exercise = workout.workout_exercises[0]
max_workout = Workout.joins(workout_exercises: :workout_sets)
.where('workout_exercises.exercise_id = ?', exercise.id)
.order('workouts.id, workout_sets.weight DESC')
.select("workouts.id, workout_sets.weight")
.uniq
#max_workout = user.workouts.max_weight(workout.workout_exercises[0])
expect(max_workout).to eq [workout]
end
Which actually throws a # exception. I've tried a bunch of things with this query, but still can't get it to work. I've ended up attempting to do it in straight SQl with the following query (excluding a user.id clause), but I get an empty result set:
max_workout = Workout.find_by_sql("
SELECT workouts.*
FROM workouts,
(SELECT DISTINCT workouts.id AS workout_id, workout_sets.weight AS weight
FROM workouts
INNER JOIN workout_exercises ON workout_exercises.workout_id = workouts.id
INNER JOIN workout_sets ON workout_sets.workout_exercise_id = workout_exercises.id
WHERE workout_exercises.exercise_id = #{exercise.id}
ORDER BY workouts.id, workout_sets.weight DESC) AS myquery
WHERE workouts.id = myquery.workout_id")
Given an Exercise instance exercise, you can select distinct workouts, nest joins with :workout_exercises and :workout_sets, filter by exercise_id, and order by workout_sets.weight as follows:
Workout.joins(:workout_exercises => :workout_sets).
where('workout_exercises.exercise_id' => exercise.id).
order('workout_sets.weight DESC').
uniq
After much work and a lot more research, this is the query yielded the desired result set:
WITH joined_table AS (
SELECT workout_sets.weight AS weight,
workouts.user_id AS user_id,
workouts.id AS workout_id,
workout_sets.id AS workout_set_id,
workout_exercises.exercise_id AS exercise_id
FROM workouts
INNER JOIN workout_exercises ON workout_exercises.workout_id = workouts.id
INNER JOIN workout_sets ON workout_sets.workout_exercise_id = workout_exercises.id
ORDER BY workout_sets.weight DESC
),
result_set AS (
SELECT MAX(x.workout_id) AS workout_id,
x.user_id,
x.weight,
x.workout_set_id,
x.exercise_id
FROM joined_table x
JOIN (SELECT p.user_id, MAX(weight) as weight
FROM joined_table p
GROUP BY p.user_id) y
ON y.user_id = x.user_id AND y.weight = x.weight
GROUP BY x.user_id, x.weight, x.workout_set_id, x.exercise_id
ORDER BY x.weight DESC)
SELECT workouts.*,
result_set.weight,
result_set.workout_set_id,
result_set.exercise_id
FROM workouts, result_set
WHERE workouts.id = result_set.workout_id
AND result_set.exercise_id = 1 -- arbitrary exercise ID
AND workouts.user_id IN (1,2) -- arbitrary set of user IDs

Where clause no longer working in Rails 3.2.3?

I upgraded to Rails 3.2.3 and all of a sudden this code no longer works:
def self.search(query, project_id, person_id)
if query
where("number LIKE ?", "%#{query}%")
elsif project_id
where("project_id LIKE ?", project_id)
elsif person_id
where("projects.person_id = ?", person_id)
else
scoped
end
end
It is the last where clause that triggers the error:
SQLite3::SQLException: no such column: projects.person_id: SELECT COUNT(DISTINCT "invoices"."id") FROM "invoices" LEFT OUTER JOIN "items" ON "items"."invoice_id" = "invoices"."id" LEFT OUTER JOIN "payments" ON "payments"."invoice_id" = "invoices"."id" WHERE "invoices"."user_id" = 1 AND (projects.person_id = '1')
In my models all the belongs_to and has_many statements are set correctly and it worked in my previous version of Rails (not sure which one that was though).
Can anybody tell me how to get this working again?
Thanks for any help.
I believe you'll have to join the projects table:
..
elsif person_id
joins(:projects).where("projects.person_id = ?", person_id)
else
..