How to use order with uniq in a PostgreSQL - ruby-on-rails

How can I use order with uniq?
auction.invoices.get_auction_invoices.item_invoices.
joins("INNER JOIN users ON users.id = invoices.usable_id").order("users.first_name").uniq
The above query gives me following error:
This is my scopes
scope :item_invoices, ->{ joins(:invoice_details).where("invoice_details.invoiceable_type = ?", "Item")}
scope :get_auction_invoices, ->{where(:id => (item_invoices.skip_cancelled_invoice + donators.skip_cancelled_invoice))}
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
LINE 1: ...oice_details.invoiceable_type = 'Item') ORDER BY users.firs...
: SELECT DISTINCT "invoices".* FROM "invoices" INNER JOIN "invoice_details" ON "invoice_details"."invoice_id" = "invoices"."id" INNER JOIN users ON users.id = invoices.usable_id WHERE "invoices"."eventable_id" = $1 AND "invoices"."eventable_type" = $2 AND "invoices"."id" IN (1132, 1131, 777, 777, 777, 3013, 3024, 3024, 3024, 3024, 3041, 3041, 3013) AND (invoice_details.invoiceable_type = 'Item') ORDER BY users.first_name

Try this:
auction.invoices.get_auction_invoices.item_invoices.\
select("invoices.*, users.*").\
joins("INNER JOIN users ON users.id = invoices.usable_id").\
order("users.first_name").\
uniq

Related

Rails is breaking SQL query when modifying order

Context
We have a Rails app that is retrieving conversations with the following raw SQL query:
SELECT sub.*,
profiles.status AS interlocutor_status
FROM (
SELECT DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
FROM "conversations"
LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id"
WHERE (post_applicant = 1234567 OR post_owner = 1234567)
AND "conversation_preferences"."user_id" = 1234567
ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
AND (last_activity_on >= '2021-01-19 04:40:22.881985')
AND (state = 'active')
ORDER BY profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25
We generate this query using the following ActiveRecord code:
def fetch
distinct = Conversation.left_outer_joins(:preferences)
.left_outer_joins(post: :category)
.left_outer_joins(:messages)
.where('post_applicant = :id OR post_owner = :id', id: current_user.id)
.where(conversation_preferences: { user_id: current_user.id })
.select(
<<-SQL.squish
DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, #{current_user.id}), NULLIF(post_applicant, #{current_user.id})) AS interlocutor_id
SQL
)
.order(:id, 'messages.created_at DESC')
Conversation.includes(post: :category)
.from(distinct, :sub)
.select('sub.*, profiles.status AS interlocutor_status')
.joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
.joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
.where.not('profiles.status' => :pending)
.order('profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC')
end
Problem
We want to stop ordering by profiles.status. To do this, we naturally removed it from the last order statement:
order('sub.unread_counter DESC, sub.last_activity_on DESC')
That's the problem. Doing that is entirely breaking the generated query, that generate an error which is irrelevant here because we don't want the modified query (note how it is different from the 1st one):
SELECT sub.*,
profiles.status AS interlocutor_status,
"conversations"."id" AS t0_r0,
"conversations"."post_id" AS t0_r1,
"conversations"."post_owner" AS t0_r2,
"conversations"."post_applicant" AS t0_r3,
"conversations"."created_at" AS t0_r4,
"conversations"."updated_at" AS t0_r5,
"posts"."id" AS t1_r0,
"posts"."title" AS t1_r1,
"posts"."description" AS t1_r2,
"categories"."id" AS t2_r0,
"categories"."name" AS t2_r1
FROM (
SELECT DISTINCT ON (conversations.id) conversations.id,
conversation_preferences.unread_counter,
left(messages.content, 50) AS last_message,
posts.id AS post_id,
messages.created_at AS last_activity_on,
categories.root_name AS site_name,
conversation_preferences.state,
COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id
FROM "conversations"
LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id"
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id"
WHERE (post_applicant = 1234567 OR post_owner = 1234567)
AND "conversation_preferences"."user_id" = 1234567
ORDER BY "conversations"."id" ASC, messages.created_at DESC
) sub
LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id"
LEFT OUTER JOIN users ON interlocutor_id = users.id
LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id
WHERE ("profiles"."status" != 'pending')
AND (last_activity_on >= '2021-01-19 05:04:06.084499')
AND (state = 'active')
ORDER BY sub.unread_counter DESC, sub.last_activity_on DESC
LIMIT 25
I know without a bit of context it'll be hard to help us but if someone knows why ActiveRecord is changing the query after trying to just remove profiles.status from the order statement, that would be awesome. Thanks in advance
NOTE: modifying the 1st raw SQL directly (from our postgres client) does works. The issue is not the first query, but maybe how ActiveRecord is handling it
Finally found a way to make it work using preload instead of includes. We wanted to avoid having seperate queries to load posts and categories but since performance is not affected by it, we don't mind it.
Here is how it look like:
Conversation.preload(post: :category)
.from(distinct, :sub)
.select('sub.*, profiles.status AS interlocutor_status')
.joins('LEFT OUTER JOIN users ON interlocutor_id = users.id')
.joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id')
.where.not('profiles.status' => :pending)
.order('sub.unread_counter DESC, sub.last_activity_on DESC')
Which generates 3 queries:
-- Query 1
SELECT sub.*, profiles.status AS interlocutor_status
FROM (
SELECT DISTINCT
....
-- Query 2
SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (............)
-- Query 3
SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (..........)
Thanks to everyone for the help in comments (max and Sebastian Palma)

Build triple UNION query using Arel (Rails 5)

user has_many tasks
I'm trying to create a 3 select UNION:
Task.from("(#{select1.to_sql} UNION #{select2.to_sql} UNION #{select3.to_sql}) AS tasks")
But with Arel.
I can easily use Arel for UNION query for 2 selects:
tasks_t = Task.arel_table
select1 = tasks_t.project('id').where(tasks_t[:id].eq(1)) # SELECT id FROM tasks WHERE tasks.id = 1
select2 = Task.joins(:user).select(:id).where(users: {id: 15}).arel # SELECT "tasks".id FROM "tasks" INNER JOIN "users" ON "users"."id" = "tasks"."user_id" WHERE "users"."id" = 15
union = select1.union(select2) # SELECT * FROM users WHERE users.id = 1 UNION SELECT * FROM users WHERE users.id = 2
union_with_table_alias = tasks_t.create_table_alias(union, tasks_t.name)
Task.from(union_with_table_alias)
# SELECT "tasks".* FROM ( SELECT id FROM "tasks" WHERE "tasks"."id" = 1 UNION SELECT "tasks"."id" FROM "tasks" INNER JOIN "users" ON "users"."id" = "tasks"."user_id" WHERE "users"."id" = $1 ) "tasks" [["id", 15]]
#=> Task::ActiveRecord_Relation [#<Task: id: 1>, #<Task: id: 2>]
How can I do it with triple union select?
select3 = tasks_t.project('id').where(tasks_t[:id].eq(3)) # SELECT id FROM tasks WHERE tasks.id = 3
Something like:
triple_union = select1.union(select2).union(select3)
triple_union_with_table_alias = tasks_t.create_table_alias(triple_union, tasks_t.name)
Task.from(triple_union_with_table_alias)
Which should roughly translate to
Task.from("(#{select1.to_sql} UNION #{select2.to_sql} UNION #{select3.to_sql}) AS tasks")
Note the above line also fails:
Caused by PG::ProtocolViolation: ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1
To summarize:
How to use Arel to build a tripple UNION query? select 1 UNION select 2 UNION select 3
Thank you
Ruby 2.5.3 Rails 5.2.4 Arel 9.0.0
You can not invoke union in an Arel::Nodes::Union, because in the method definition the ast of both objects is called. And the result of a union operation doesn't respond to ast:
def union(operation, other = nil)
if other
node_class = Nodes.const_get("Union#{operation.to_s.capitalize}")
else
other = operation
node_class = Nodes::Union
end
node_class.new(self.ast, other.ast)
end
What you can do is to manually call Arel::Nodes::Union, passing as any of those arguments the result of a union:
Arel::Nodes::Union.new(Arel::Nodes::Union.new(select1, select2), select3)

Rails | Add condition in join query

Is it possible, to add condition in join query?
For example I want to build next query:
select * from clients
left join comments on comments.client_id = clients.id and comments.type = 1
Client.joins(:comments).all generates only:
select * from clients
left join comments on comments.client_id = clients.id
PS. Client.joins("LEFT JOIN comments on comments.client_id = clients.id and comment.type = 1") isn't nice.
You can do:
Client.left_outer_joins(:comments)
.where(comments: { id: nil })
.or(Client.left_outer_joins(:comments)
.where.not(comments: { id: nil })
.where(comments: { type: 1 }))
what gives you something equivalent to what you want:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id"
WHERE "comments"."id" IS NULL
OR ("comments"."id" IS NOT NULL) AND "comments"."type" = 1
UPDATE
Actually, this does not work because rails close the parenthesis leaving outside the evaluation of type.
UPDATE 2
If yours comment types are few and is not probable its values will change, you can solve it with this way:
class Client < ApplicationRecord
has_many :comments
has_many :new_comments, -> { where comments: { type: 1 } }, class_name: Comment
has_many :spam_comments, -> { where comments: { type: 2 } }, class_name: Comment
end
class Comment < ApplicationRecord
belongs_to :client
end
With this new relationships in your model now you can do:
Client.left_joins(:comments)
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id"
Client.left_joins(:new_comments)
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
/*[["type", 1]]*/
Client.left_joins(:spam_comments)
gives the same query:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
/*[["type", 2]]*/
Client.left_joins(:new_comments).where name: 'Luis'
gives:
SELECT "clients".*
FROM "clients"
LEFT OUTER JOIN "comments"
ON "comments"."client_id" = "clients"."id" AND "comments"."type" = ?
WHERE "clients"."name" = ?
/*[["type", 1], ["name", "Luis"]]*/
NOTE:
I try to use a parameter in the original has_many relationship like...
has_many :comments, -> (t) { where comments: { type: t } }
but this give me an error:
ArgumentError: The association scope 'comments' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
I ended up doing this:
meals_select = Meal.left_outer_joins(:dish_quantities).arel
on_expression = meals_select.source.right.first.right.expr
on_expression.children.push(DishQuantity.arel_table[:date].eq(delivery_date))
on_expression.children.push(DishQuantity.arel_table[:dish_id].eq(Dish.arel_table[:id]))
ON "dish_quantities"."meal_id" = "meals"."id" AND "dish_quantities"."date" = '2022-08-05' AND "dish_quantities"."dish_id" = "dishes"."id"
Client.joins(:comments).where(comments: {type: 1})
Try this

Rails produces invalid SQL for count on complex query

This is a repost from https://github.com/rails/rails/issues/22491
Given this statement:
#items = DailyList.joins(:bar)
.select("daily_lists.*, (point(12, 34) <#> bars.coords) as bar_distance")
.order("bar_distance ASC")
.group('bars.id, daily_lists.id')
.limit(3)
it produces the following SQL:
SELECT daily_lists.*, (point(12, 34) <#> bars.coords) bar_distance
FROM "daily_lists"
INNER JOIN "bars" ON "bars"."id" = "daily_lists"."bar_id"
GROUP BY bars.id, daily_lists.id
ORDER BY bar_distance ASC
LIMIT 3
Now we want to check how much records we've got, #items.count:
SELECT COUNT(daily_lists.*, (point(12, 34) <#> bars.coords) as bar_distance) AS count_daily_lists_all_point_12_34_bars_coords_as_bar_distance, bars.id, daily_lists.id AS bars_id_daily_lists_id
FROM "daily_lists"
INNER JOIN "bars" ON "bars"."id" = "daily_lists"."bar_id"
GROUP BY bars.id, daily_lists.id
ORDER BY bar_distance ASC
LIMIT 3
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: syntax error at or near "as"
LINE 1: ...NT(daily_lists.*, (point(12, 34) <#> bars.coords) as bar_dis...
Now I'm trying to use :all as a workaround, #items.count(:all):
SELECT COUNT(*) AS count_all, bars.id, daily_lists.id AS bars_id_daily_lists_id
FROM "daily_lists"
INNER JOIN "bars" ON "bars"."id" = "daily_lists"."bar_id"
GROUP BY bars.id, daily_lists.id
ORDER BY bar_distance ASC
LIMIT 3
PG::UndefinedColumn: ERROR: column "bar_distance" does not exist
Guys... what to do?
The "as bar_distance" is the problem, I think. You cannot alias within a count.
#items = DailyList.joins(:bar)
.select("daily_lists.*, (point(12, 34) <#> bars.coords) as bar_distance")
.order("bar_distance ASC")
.group('bars.id, daily_lists.id')
.limit(3)
#item_count = DailyList.joins(:bar)
.select("daily_lists.*, (point(12, 34) <#> bars.coords)")
.group('bars.id, daily_lists.id')
.limit(3)
.count

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