Rails is breaking SQL query when modifying order - ruby-on-rails

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)

Related

Eager loading model instances using single SQL statement with Mobility table backend

Using Rails 7 and Mobility 1.2.9:
# config/initializers/mobility.rb
Mobility.configure do |config|
config.plugins do
backend :table
active_record
reader
writer
backend_reader
query
cache
dirty
presence
end
end
# app/models/content.rb
class Content < ApplicationRecord
extend Mobility
translates :slug, :title
end
How to find contents by slug, instantiate them into Content objects and query their translated attributes using one single SQL statement (for an unchanging locale)? The examples below with and without eager_loading run 2 SQL statements.
> content = Content.i18n.find_by(slug: "foo")
Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'foo' LIMIT $1 [["LIMIT", 1]]
> content.title
Content::Translation Load (0.3ms) SELECT "content_translations".* FROM "content_translations" WHERE "content_translations"."content_id" = $1 [["content_id", "..."]]
> content = Content.i18n.eager_load(:translations).find_by(slug: "foo")
SQL (0.6ms) SELECT DISTINCT "contents"."id" FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' LIMIT $1 [["LIMIT", 1]]
SQL (0.8ms) SELECT "contents"."id" AS t0_r0, "contents"."created_at" AS t0_r1, "contents"."updated_at" AS t0_r2, ..., "content_translations"."id" AS t1_r0, "content_translations"."locale" AS t1_r1, "content_translations"."created_at" AS t1_r2, "content_translations"."updated_at" AS t1_r3, "content_translations"."slug" AS t1_r4, "content_translations"."title" AS t1_r5, "content_translations"."content_id" AS t1_r6 FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' AND "contents"."id" = $1 [["id", "..."]]
> content.title
# no DB statement, but there were 2 statements at instantiation
NB: I do not want to pluck attributes but instead to create model instances.

Rails 4 - Eager loading with multiple ON conditions

I have an Article and each Article has exactly one User and either zero or one ArticleVote.
My code is:
#articles = Article
.eager_load(:user)
.eager_load(:article_vote)
.where(article_votes: { user_id: session[:user_id]})
.order(created_at: :desc)
Which produces the following SQL:
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id`
WHERE `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
The problem with this query is if no vote exists (which sometimes it won't), then no records are returned. What I really need is this (note the where is gone and the condition is moved to the left join):
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id` and `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
I saw in the documentation I can do something like:
has_one :article_vote, -> { where(user_id: 1) }
Which will put it in the left join vs the where, but this doesn't let me specify the user_id at query time, which makes it not viable.
Can this be done?

How to use order with uniq in a PostgreSQL

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

How to access joined record from Rails LEFT OUTER JOIN

Howdy I found plenty of examples on how to use LEFT OUTER JOIN, but I can't seem to find a way to access what I have joined. Here is what I mean:
List.featured.
joins(
"LEFT OUTER JOIN follows ON (
follows.followable_id = lists.id AND
follows.followable_type = 'List' AND
follows.user_id = #{current_user.id})"
).map { |list|
list.follows # <-- returns all follows, not only the ones from current_user
...
In the example I get the follows (it seems) with the join, but then how can I access them? The follows relation will just give me all follows for that list it seems.
Or maybe my mind is fogged :) Thanks!
To eager load follows, you can call includes():
List.featured.includes(:follows).where(:follows => { :user_id => current_user.id })
It generates queries like this:
SELECT
"lists"."id" AS t0_r0,
"lists"."is_featured" AS t0_r1,
"lists"."created_at" AS t0_r2,
"lists"."updated_at" AS t0_r3,
"follows"."id" AS t1_r0,
"follows"."followable_id" AS t1_r1,
"follows"."followable_type" AS t1_r2,
"follows"."user_id" AS t1_r3,
"follows"."created_at" AS t1_r4,
"follows"."updated_at" AS t1_r5
FROM
"lists"
LEFT OUTER JOIN
"follows"
ON
"follows"."followable_id" = "lists"."id" AND
"follows"."followable_type" = 'List'
WHERE
"lists"."is_featured" = 't' AND
"follows"."user_id" = 1

Squeel query comparing 2 variables

Using squeel, I'm able to generate the following request
Payment.joins(:account => :preference).where({:account => {:preference => {:currency => :currency } } }).to_sql
=> SELECT "payments".* FROM "payments" INNER JOIN "accounts" ON "accounts"."id" = "payments"."account_id" INNER JOIN "preferences" ON "preferences"."account_id" = "accounts"."id" WHERE "preferences"."currency" = "preferences"."currency"
However, how to get? (CHANGE IN CAPITAL)
=> SELECT "payments".* FROM "payments" INNER JOIN "accounts" ON "accounts"."id" = "payments"."account_id" INNER JOIN "preferences" ON "preferences"."account_id" = "accounts"."id" WHERE "preferences"."currency" = "PAYMENTS"."currency"
If the solution work fine with meta_where as well that's even better ;-)
Thanks to IRC guys hron84, injekt and SIGe
Here's the solution :
Payment.joins(:account => :preference).where{account.preference.currency == ~currency }.to_sql

Categories

Resources