rails: incorrect default scope merging for nested associations

I’ve run into a problem with 4.0.1 related to scope merging of nested associations.

Example model hierarchy:

class Window < ActiveRecord::Base
  belongs_to :room

  scope :glazed,  -> { where( has_glass: 1 ) }
  scope :having_handles, -> { where( has_handle: 1 ) }

  # only glazed windows count by default
  default_scope { glazed }
end
class Room < ActiveRecord::Base
  belongs_to :house
  has_many :windows

  scope :having_openable_windows, ->( ) {
    # use unscoped to avoid duplicate application of the default scope of Window
    joins(:windows).merge( Window.unscoped.having_handles ).group( 'rooms.id' )
  }

  # only rooms with openable windows count by default
  default_scope { having_openable_windows }
end
class House < ActiveRecord::Base
  has_many :rooms
  has_many :windows, through: :rooms

  scope :having_windows, ->( ) {
    joins(:windows).group( 'houses.id' )
  }

  # only houses with [rooms having openable] windows count by default
  default_scope { having_windows }
end

When generating queries, here’s how the Window scopes are applied in 4.0.1:

Window.all.to_sql:

SELECT `windows`.*
FROM   `windows`
WHERE  `windows`.`has_glass` = 1 

This is correct, the default scope (which applies glazed scope) gets included in WHERE section.

Room.all.to_sql:

SELECT `rooms`.*
FROM   `rooms`
       INNER JOIN `windows`
               ON `windows`.`room_id` = `rooms`.`id`
                  AND `windows`.`has_glass` = 1
WHERE  `windows`.`has_handle` = 1
GROUP  BY rooms.id   

This could still be correct. The default scope of Window now gets included in the JOIN section of windows table, and the additional having_handles scope gets included in WHERE.

House.all.to_sql:

SELECT `houses`.*
FROM   `houses`
       INNER JOIN `rooms`
               ON `rooms`.`house_id` = `houses`.`id`
                  AND `windows`.`has_handle` = 1
       INNER JOIN `windows`
               ON `windows`.`room_id` = `rooms`.`id`
                  AND `windows`.`has_glass` = 1
GROUP  BY houses.id  

This no longer looks correct.

The having_handles scope should be included in either the JOIN section of windows table or the overall WHERE section, but instead it gets included in the JOIN of rooms where it makes no sense and causes an error.

Since this was not a problem before 4.0.1 when any needed association scopes had to be explicitly merged on each level (and the explicit merges worked correctly!), could this be a problem with how the automatic default scope inclusion works in 4.0.1?

About this issue

  • Original URL
  • State: closed
  • Created 11 years ago
  • Comments: 15 (3 by maintainers)

Most upvoted comments

It would be great to have this re-opened. Looks like it’s still happening for us on Rails 4.2.5 and 4.2.6.

Quick example:

class User < ActiveRecord::Base
  scope :include_email, -> { joins(:contacts).where("user_contacts.type = 'primary'") }
  default_scope { include_email }

  has_many :contacts
end

class UserContact < ActiveRecord::Base
  belongs_to :user
end

class Order < ActiveRecord::Base
  belongs_to :user
end

Then if we try to join to user it applies the where but not the join.

Order.joins(:user)

and it returns

D, [2016-06-10T13:25:07.357310 #53667] DEBUG -- :   Order Load (6.8ms)  SELECT "orders".* FROM "orders" INNER JOIN "users" ON "users"."id" = "orders"."user_id" AND (user_contacts.type = 'primary')
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "user_contacts"
LINE 1: ..."users" ON "users"."id" = "orders"."user_id" AND (user_conta...
                                                             ^
: SELECT "orders".* FROM "orders" INNER JOIN "users" ON "users"."id" = "orders"."user_id" AND (user_contacts.type = 'primary')

I can reproduce the same issue on rails 5.0.0.1

Same on Rails 5.1.2

I’m also having this issue rails 5.0.0