How to Fix Rails 6.1 Relation `merge` Deprecation

How to Fix Rails 6.1 Relation `merge` Deprecation

Recently, while working on a Rails 6.1 to 7.0 upgrade, we encountered the following deprecation warning regarding changes made to ActiveRecord::Relation’s merge method:

"Merging (#{node.to_sql}) and (#{ref.to_sql}) no longer maintains both conditions, and will be replaced by the latter in Rails 7.0. To migrate to Rails 7.0's behavior, use relation.merge(other, rewhere: true)."

In this article, we will talk about the expected behavior of merge, how it has changed and what to do in order to use the new behavior if you find yourself looking at this deprecation.

What is merge?

Let’s first take a look at the expected behavior of merge opens a new window .

Using merge is actually pretty useful for when you want to combine two complex relation objects that each contain multiple conditions, joins, and/or includes.

You can call merge on a relation object and pass another relation object as its argument. Rails then merges the two relation objects into a new relation object that includes the conditions, joins, includes, and other clauses from both relation objects.

Say you’d like to have two relation objects, posts and featured_posts

posts = Post.where(published: true)
featured_posts = Post.where(featured: true)

However, instead you would like to return a single relation object of all published posts that have been featured, you can do:

Post.where(published: true).merge(Post.where(featured: true))

This will execute a single query with the AND clause and return a single relation object

SELECT "posts".* FROM "posts" WHERE "posts"."published" = ? AND "posts"."featured" = ?

=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2023-05-07 18:51:11.370457000 +0000", updated_at: "2023-05-07 18:51:46.974839000 +0000">, #<Post id: 3, title: "testing 3", body: "This is the third post", published: true, featured: true, author_id: 3, created_at: "2023-05-07 18:51:11.373971000 +0000", updated_at: "2023-05-07 18:51:54.961656000 +0000">]>

What is even better is that this new relation object can then be further queried and/or executed upon if necessary.

This is more efficient as you can avoid duplicating any conditions or joins.

New behavior in Rails 7.0

According to the Ruby on Rails 7.0 Release notes opens a new window :

"Merging conditions on the same column no longer maintain both conditions, and will be consistently replaced by the latter condition."

With the following examples:

  # Rails 6.1 (IN clause is replaced by merger side equality condition)
  Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
  # Rails 6.1 (both conflict conditions exists, deprecated)
  Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => []
  # Rails 6.1 with rewhere to migrate to Rails 7.0's behavior
  Author.where(id: david.id..mary.id).merge(Author.where(id: bob), rewhere: true) # => [bob]
  # Rails 7.0 (same behavior with IN clause, mergee side condition is consistently replaced)
  Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
  Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [bob]

So what does this all mean?

In the new behavior, when attempting to merge two Active Record relation objects that have conflicting conditions on the same column, the conditions of the first relation object (mergee) will be discarded and only the second relation object’s (merger) conditions will be retained. This creates an unexpected result since part of the query is ignored, only returning the latter condition’s results.

In some cases, even if conditions are performed on the same column, there will be no deprecation warning but the new behavior of merge is maintained, which means the returned relation object will only satisfy the mergers condition.

Cases that do not raise a warning

Queries written using strings as the conditional statements

For example:

Post.where("created_at <= ?", 3.months.ago).merge(Post.where("created_at >= ?", 3.months.ago))

The generated query maintains both conditions combined with an AND clause:

SELECT "posts".* FROM "posts" WHERE (created_at <= '2023-02-08 14:53:26.967529') AND (created_at >= '2023-02-08 14:53:26.970391')

Or let’s say we used a string for one condition and a key/value pair for the mergee:

Post.where("created_at <= ?", 3.months.ago).merge(Post.where(created_at: 3.months.ago))
SELECT "posts".* FROM "posts" WHERE (created_at <= '2023-02-09 14:43:20.344537') AND "posts"."created_at" = ?

Both examples do not raise the deprecation because Rails does not know that the same attributes are being compared, hence there is no conflict Rails is aware of, and they behave the same way in both Rails 6.1 and 7.0.

The merger and the mergee both perform IN or = on the same attribute

In this case, merge will behave as expected in Rails 7 by replacing the mergee with the mergers condition:

Post.where(created_at: "2023-05-02").merge(Post.where(created_at: "2022-05-08"))
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" = ?
=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2022-05-08 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:16:51.947757000 +0000">]>

-or-

Post.where(created_at: ["2023-05-02", "2023-02-01"]).merge(Post.where(created_at: "2022-05-08"))
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" = ?
=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2022-05-08 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:16:51.947757000 +0000">]>

Cases that raise a deprecation warning

This occurs when there is a combination of clauses performed on the same column, causing a conflict in the query.

For example:

Post.where(created_at: 1.year.ago..3.months.ago).merge(Post.where(created_at: 1.week.ago..))

> ActiveSupport::DeprecationException (DEPRECATION WARNING: Merging ("posts"."created_at" BETWEEN ? AND ?) and ("posts"."created_at" >= ?) no longer maintain both conditions, and will be replaced by the latter in Rails 7.0. To migrate to Rails 7.0's behavior, use `relation.merge(other, rewhere: true)`. (called from irb_binding at (irb):19))

If we take a look at the query generated by the mergee, we can see a BETWEEN as well as a >= condition:

SELECT "posts".* FROM "posts" WHERE "posts"."created_at" BETWEEN ? AND ? AND "posts"."created_at" >= ?

Therefore, the merger condition creates a conflict and raises the deprecation.

To fix this deprecation warning to use the new Rails 7 behavior, the rewhere: true option must be passed to merge:

Post.where(created_at: 1.year.ago..3.months.ago).merge(Post.where(created_at: 1.week.ago...), rewhere: true)

which will then query the merger condition and return:

SELECT "posts".* FROM "posts" WHERE "posts"."created_at" >= ?
=> #<ActiveRecord::Relation [#<Post id: 3, title: "testing 3", body: "This is the third post", published: true, featured: true, author_id: 3, created_at: "2023-05-02 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:09:51.369571000 +0000">]>

In the case where there is a need to maintain both conditions, writing a statement similar to the first example would be best:

Post.where("created_at IN ? AND ?", 1.year.ago, 3.months.ago).merge(Post.where("created_at >= ?", 1.week.ago))

With both conditions maintained:

SELECT "posts".* FROM "posts" WHERE (created_at BETWEEN '2022-05-08 17:06:17.771827' AND '2023-02-08 17:06:17.772156') AND (created_at >= '2023-05-01 17:06:17.773653')

Conclusion

Utilizing the merge method to build efficient and complex queries can be extremely beneficial. However, with the recent changes introduced in Rails 7.0, you might find yourself looking at unexpected results, or a deprecation warning.

It is important to further investigate what Active Record is trying to do through the SQL queries generated to further understand the best way to solve the issue and get the expected results.

Need help upgrading Ruby or Rails to the latest stable version? We have some availability to help your team upgrade Ruby/Rails opens a new window !

Get the book