JOIN with COALESCE and ORDER BY in Rails - ruby-on-rails

I just ran into an issue with Rails where I couldn't properly use JOIN and multiple ORDER BYs (with one of them being a COALESCE function) in the same query. Is this something that is illegal in SQL (doubtful) or is it just an issue with Rails' implementation? Also, how can I get around it?
# Works!
Post.joins(:author).order("COALESCE(title, '---') DESC")
=> SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "posts"."author_id" = "authors"."id" ORDER BY COALESCE(title DESC, '--')
# Fails - syntax error in SQL
Post.joins(:author).order("COALESCE(title, '---') DESC").last #last should automatically apply an ORDER BY id DESC;
=> SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "posts"."author_id" = "authors"."id" ORDER BY COALESCE(title DESC, '--') ASC LIMIT 1

when applying order("COALESCE(table.column, fallback_value) asc/desc") to my query I started getting the following error
ActiveRecord::UnknownAttributeReference: Query method called with non-attribute argument(s):
this was resolved by wrapping the order argument with Arel.sql as suggested by this Answer
order(Arel.sql("COALESCE(table.column, fallback_value) asc/desc"))
For more information on why this works I found this article from Thoughtbot to be helpful.

It looks like .last applies ASC LIMIT 1 to the SQL query. This seems to confuse the rails syntax parser and it moves the DESC into a strange place, which causes the error.
SELECT "discussion_topics".* FROM "discussion_topics" ORDER BY COALESCE(title DESC, '---') ASC LIMIT 1
as opposed to
SELECT "discussion_topics".* FROM "discussion_topics" ORDER BY COALESCE(title, '---') DESC
You could probably change the DESC to an ASC and use .first safely.

With Postgres I would try
Post.select("COALESCE(title, '---') DESC").joins(:author).order("COALESCE(title, '---') DESC").limit(1)

Related

Why does ActiveRecord find_by return nil when using it together with select?

I found the following kind of usage of ActiveRecord#find_by written in a Rails 4.1 project:
booking = Booking.find_by(member_id: Member.where(id: 1).select(:id))
However, this query returned nil after upgrading the project to Rails > 4.2.
In Rails 4.2 the above query generates the following SQL:
SELECT "bookings".* FROM "bookings" WHERE "bookings"."member_id" = $1 LIMIT 1 [["member_id", nil]]
A 'Booking' belongs to a 'Member' and a 'Member' has many 'Booking'.
Does anyone know or see why? I would be interested in an explanation.
Replacing the select with pluck brings back the expected behavior:
booking = Booking.find_by(member_id: Member.where(id: 1).pluck(:id))
Generated query: SELECT "bookings".* FROM "bookings" WHERE "bookings"."member_id" = 1 LIMIT 1
Edit: A member record with ID 1 exists in the database.
Member.where(id: 1).select(:id) returns the following result and SQL statement with Rails 4.2:
=> #<ActiveRecord::Relation [#<Member id: 1>]>
SELECT "members"."id" FROM "members" WHERE "members"."id" = $1 [["id", 1]]
Read this
[Rails-ORM] find_by vs. where

Remove duplicated records keeping last usign ActiveRecord

I've been trying to remove the records that are duplicated (same value in the column shopify_order_id) keeping the most recent one.
I wrote it in sql:
select orders.id from (
select shopify_order_id, min(shopify_created_at) as min_created
from orders group by shopify_order_id having count(*) > 1 limit 5000
) as keep_orders
join orders
on
keep_orders.shopify_order_id = orders.shopify_order_id and
orders.shopify_created_at <> keep_orders.min_created
and now I'm trying to get it to Active Record but can't seem to join the two parts.
The first nested select is
Order.select('shopify_order_id, MIN(shopify_created_at) as min_created').
group(:shopify_order_id).
having('count(*) > 1').
limit(5000)
but then the following doesn't work:
Order.select('orders.id').from(keep_orders, :keep_orders).
joins('orders ON keep_orders.shopify_order_id = orders.shopify_order_id').
where.not('orders.shopify_created_at = keep_orders.min_created')
it builds the query:
SELECT orders.id FROM (SELECT shopify_order_id, MIN(shopify_created_at) as min_created FROM "orders" GROUP BY "orders"."shopify_order_id" HAVING (count(*) > 1) LIMIT $1) keep_orders orders ON keep_orders.shopify_order_id = orders.shopify_order_id WHERE NOT (orders.shopify_created_at = keep_orders.min_created) ORDER BY "orders"."id" ASC LIMIT $2 [["LIMIT", 5000], ["LIMIT", 1]]
which is missing the keyword join.
Any help on how to refactor the query/do it in another way would be more than appreciated.
If you call joins with a string SQL fragment you need to specify the type of join you want:
Order.select('orders.id').from(keep_orders, :keep_orders)
.joins('JOIN orders ON keep_orders.shopify_order_id = orders.shopify_order_id')
.where.not('orders.shopify_created_at = keep_orders.min_created')

What is default order when there is no explicit defined order?

In Rails ActiveRecord when I do something like that event_instances.order(:created_at) and not specifying any order which order is default DESC or ASC ?
Thanks in advance.
According the user manuals for rails, when you've specified a symbol, sorting is setup to ASC, when string, default order, which is set by database up, is specified:
User.order(:name)
=> SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
User.order('name')
=> SELECT "users".* FROM "users" ORDER BY name
Sort orders in DBs:
For Postgres:
ASC order is the default.
For MySQL 5.7:
The default is ascending order; this can be specified explicitly using the ASC keyword.
For SQLite:
If neither ASC or DESC are specified, rows are sorted in ascending (smaller values first) order by default.
So for all the main DBs dafault order is ASC

Rails `find_by` returning huge ID

Curious if anyone knows the intricacies of find_by since I've checked documentation and been unable to find info.
I know that find is used to find by primary keys like:
#user = User.find(params[:id]), returning the correct user.
Before I corrected my code it was #user = User.find_by(params[:id]) and returned a user with an ID way above the number of users in my DB.
Can anyone help me understand what is happening under the hood? What does find_by search by default when a parameter is omitted that is returning this strange user object?
find_by_field(value) is equivalent to where(field: value) but is not supposed to be used without appending a field name to the method like you mentioned. Moreover it returns only the first matching value. For example instead of doing User.where(name: 'John').limit(1) you can use: User.find_by_name 'John'.
On my side, using find_by with postgresql raises an error, when find_by_id does work:
User.find_by(1)
SELECT "users".* FROM "users" WHERE (1) ORDER BY "users"."email" ASC LIMIT 1
PG::DatatypeMismatch: ERROR: argument of WHERE must be type boolean, not type integer
User.find_by_id 1
SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."email" ASC LIMIT 1
<User id: 1, ...
User.where(id: 1) # note that there is no LIMIT 1 in the generated SQL
SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."email" ASC
You can use gem query_tracer to see the generated SQL or check this thread.
Please take a look here.
Here is an excerpt for find_by:
Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
If no record is found, returns nil.
Post.find_by name: 'Spartacus', rating: 4
Post.find_by "published_at < ?", 2.weeks.ago
Is that what you were looking for?
UPDATE
User.find_by(3) is equivalent to User.where(3).take
Here's the output from the console
pry(main)> User.where(3).take
#=> User Load (0.3ms) SELECT `users`.* FROM `users` WHERE (3) LIMIT 1
It's look like rails return to you an id of object in memory. It's like query User.find(params[:id]).object_id. But why it's happens? I try did same on my app with 4.2.4 version and all goes fine

Custom scope in ActiveRecord - Reverse sorting is produced with invalid syntax

Rails version 4.1.6, Postgres version not important.
I use a custom sorting, where strings come before integers and then integers get sorted as numbers:
sample sorting:
A0101
BD330
BE124
1
2
3
10
Since there is no direct way to achieve this with the query interface, I've found this postgres specific syntax which, in general, works fine:
default_scope {
order("substring(entries.code, '[^0-9_].*$') ASC").
order("(substring(entries.code, '^[0-9]+'))::int ASC")
}
For example, to get the first record:
2.0.0p247 :001 > Entry.first
Entry Load (3.6ms) SELECT "entries".* FROM "entries" ORDER BY substring(entries.code, '[^0-9_].*$') ASC, (substring(entries.code, '^[0-9]+'))::int ASC LIMIT 1
=> #<Entry id: ...............>
However, when I want to do a reverse search, I get some DESC words raining all over the query string... This is quite annoying since I haven't found a way yet to dispose off them:
2.0.0p247 :002 > Entry.last
Entry Load (0.8ms) SELECT "entries".* FROM "entries" ORDER BY substring(entries.code DESC, '[^0-9_].*$') DESC, (substring(entries.code DESC, '^[0-9]+'))::int DESC LIMIT 1
PG::Error: ERROR: syntax error at or near "DESC"
LINE 1: ... FROM "entries" ORDER BY substring(entries.code DESC, '[^0...
^
: SELECT "entries".* FROM "entries" ORDER BY substring(entries.code DESC, '[^0-9_].*$') DESC, (substring(entries.code DESC, '^[0-9]+'))::int DESC LIMIT 1
ActiveRecord::StatementInvalid: PG::Error: ERROR: syntax error at or near "DESC"
LINE 1: ... FROM "entries" ORDER BY substring(entries.code DESC, '[^0...
To be more specific, which I believe is not necessary, I would like to get rid of those DESC within the substring() methods...
EDIT:
I see in definition of reverse_sql_order, that the string is split at the commas , and ASC or DESC is applied there...
Using extensive database-oriented functions in a Rails project is never a good idea. Those kind of composite statements can drive you insanely crazy.
order("substring(entries.code, '[^0-9_].*$') ASC").
order("(substring(entries.code, '^[0-9]+'))::int ASC")
IMHO, the simplest and more effective solution is an helper column. Define, for instance, a table column called weight with type integer.
Define a model callback that, every time you save an object, stores in the column 0 if the value of the sorting field is a string, the digit if the value is a number. Here's your sort index.
Run the sort queries against that weight column. You can even index the attribute, and your queries will be much cleaner and faster. You will also be able to sort by DESC or ASC with no complexity at all.

Resources