Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ActiveRecord::QueryMethods#in_order_of. #42061

Merged
merged 1 commit into from
Aug 4, 2021

Conversation

kddnewton
Copy link
Contributor

Summary

This allows you to specify an explicit order that you'd like records
returned in based on a SQL expression. By default, this will be accomplished
using a case statement, as in:

Post.in_order_of(:id, [3, 5, 1])

will generate the SQL:

SELECT "posts".* FROM "posts" ORDER BY CASE "posts"."id" WHEN 3 THEN 1 WHEN 5 THEN 2 WHEN 1 THEN 3 ELSE 4 END ASC

However, because this functionality is built into MySQL in the form of the
FIELD function, that connection adapter will generate the following SQL
instead:

SELECT "posts".* FROM "posts" ORDER BY FIELD("posts"."id", 1, 5, 3) DESC

This work largely inspired by the new addition to Enumerable here: #41333. cc @dhh

@dhh
Copy link
Member

dhh commented Apr 23, 2021

Post.find([3, 5, 1]) already does this 😄. It's that original behavior that inspired in_order_of! See here:

@dhh dhh closed this Apr 23, 2021
@kddnewton
Copy link
Contributor Author

@dhh yeah but that returns an array of objects. This returns a relation that you can keep chaining, and add plucks onto the end.

@dhh dhh reopened this Apr 23, 2021
@dhh
Copy link
Member

dhh commented Apr 23, 2021

Ah I see. Do you have an example where you know all the IDs you want to fetch, but you want to chain it further still? How would this interact with ORDER etc?

@kddnewton
Copy link
Contributor Author

kddnewton commented Apr 23, 2021

@dhh I probably wouldn't be ordering by ID. More likely you have something like:

Post.in_order_of(:type, %w[Draft Published Archived]).order(:created_at).pluck(:name)

which generates

SELECT posts.name
FROM posts
ORDER BY
  CASE posts.type WHEN 'Draft' THEN 1 WHEN 'Published' THEN 2 WHEN 'Archived' THEN 3 ELSE 4 END ASC,
  posts.created_at ASC

in MySQL, it's

SELECT posts.name
FROM posts
ORDER BY
  FIELD(posts.type, 'Archived', 'Published', 'Draft') DESC,
  posts.created_at ASC

It's just another order value, so it goes into the same array.

This allows you to specify an explicit order that you'd like records
returned in based on a SQL expression. By default, this will be accomplished
using a case statement, as in:

```ruby
Post.in_order_of(:id, [3, 5, 1])
```

will generate the SQL:

```sql
SELECT "posts".* FROM "posts" ORDER BY CASE "posts"."id" WHEN 3 THEN 1 WHEN 5 THEN 2 WHEN 1 THEN 3 ELSE 4 END ASC
```

However, because this functionality is built into MySQL in the form of the
`FIELD` function, that connection adapter will generate the following SQL
instead:

```sql
SELECT "posts".* FROM "posts" ORDER BY FIELD("posts"."id", 1, 5, 3) DESC
```

*Kevin Newton*
@kaspth kaspth merged commit 14540a5 into rails:main Aug 4, 2021
@kaspth
Copy link
Contributor

kaspth commented Aug 4, 2021

Rad as hell! Thanks 🙌

@@ -137,6 +137,11 @@ def supports_insert_on_duplicate_update?
true
end

def field_ordered_value(column, values)
field = Arel::Nodes::NamedFunction.new("FIELD", [column, values.reverse])
Arel::Nodes::Descending.new(field)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this avoid reversing values with Arel::Nodes::Ascending?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't 100% remember why I did this, but it has to do with mysql putting nulls first or last, so it needed to be this way. I'll see if I can find out why.

@kddnewton kddnewton deleted the ar-in-order-of branch August 4, 2021 15:52
@@ -34,7 +60,7 @@

*Luis Vasconcellos*, *Eileen M. Uchitelle*

* Fix `eager_loading?` when ordering with `Hash` syntax.
* Fix `eager_loading?` when ordering with `Hash` syntax
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaspth I think this change was unintended.
I've made a PR to add the dot back (and add some others).
#42967

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p8 Indeed, thanks!

casperisfine pushed a commit to Shopify/rails that referenced this pull request Dec 17, 2021
Fix: rails#43903
Ref: rails#42061

Simply don't generate the case statement if the order list is empty
and fallback to default ordering.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants