Skip to content
  • Ryuta Kamizono's avatar
    10b36e81
    Fix incorrect result when eager loading with duplicated through association with join scope · 10b36e81
    Ryuta Kamizono authored
    I had found the issue while working on fixing #33525.
    
    That is if duplicated association has a scope which has `where` with
    explicit table name condition (e.g. `where("categories.name": "General")`),
    that condition in all duplicated associations will filter the first one
    only, other all duplicated associations are not filtered, since
    duplicated joins will be aliased except the first one (e.g.
    `INNER JOIN "categories" "categories_categorizations"`).
    
    ```ruby
    class Author < ActiveRecord::Base
      has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
      has_many :general_posts, through: :general_categorizations, source: :post
    end
    
    authors = Author.eager_load(:general_categorizations, :general_posts).to_a
    ```
    
    Generated eager loading query:
    
    ```sql
    SELECT "authors"."id" AS t0_r0, ... FROM "authors"
    
    -- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
    LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
    INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
    
    -- `has_many :general_posts, through: :general_categorizations, source: :post`
    ---- duplicated `through: :general_categorizations` part
    LEFT OUTER JOIN "categorizations" "general_categorizations_authors_join" ON "general_categorizations_authors_join"."author_id" = "authors"."id"
    INNER JOIN "categories" "categories_categorizations" ON "categories_categorizations"."id" = "general_categorizations_authors_join"."category_id" AND "categories"."name" = ? -- <-- filtering `"categories"."name" = ?` won't work
    ---- `source: :post` part
    LEFT OUTER JOIN "posts" ON "posts"."id" = "general_categorizations_authors_join"."post_id"
    ```
    
    Originally eager loading with join scope didn't work before Rails 5.2
    (#29413), and duplicated through association with join scope raised a
    duplicated alias error before alias tracking is improved in 590b045e.
    
    But now it will potentially be got incorrect result instead of an error,
    it is worse than an error.
    
    To fix the issue, it makes eager loading to deduplicate / re-use
    duplicated through association if possible, like as `preload`.
    
    ```sql
    SELECT "authors"."id" AS t0_r0, ... FROM "authors"
    
    -- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
    LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
    INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
    
    -- `has_many :general_posts, through: :general_categorizations, source: :post`
    ---- `through: :general_categorizations` part is deduplicated / re-used
    LEFT OUTER JOIN "posts" ON "posts"."id" = "categorizations"."post_id"
    ```
    
    Fixes #32819.
    10b36e81
    Fix incorrect result when eager loading with duplicated through association with join scope
    Ryuta Kamizono authored
    I had found the issue while working on fixing #33525.
    
    That is if duplicated association has a scope which has `where` with
    explicit table name condition (e.g. `where("categories.name": "General")`),
    that condition in all duplicated associations will filter the first one
    only, other all duplicated associations are not filtered, since
    duplicated joins will be aliased except the first one (e.g.
    `INNER JOIN "categories" "categories_categorizations"`).
    
    ```ruby
    class Author < ActiveRecord::Base
      has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
      has_many :general_posts, through: :general_categorizations, source: :post
    end
    
    authors = Author.eager_load(:general_categorizations, :general_posts).to_a
    ```
    
    Generated eager loading query:
    
    ```sql
    SELECT "authors"."id" AS t0_r0, ... FROM "authors"
    
    -- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
    LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
    INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
    
    -- `has_many :general_posts, through: :general_categorizations, source: :post`
    ---- duplicated `through: :general_categorizations` part
    LEFT OUTER JOIN "categorizations" "general_categorizations_authors_join" ON "general_categorizations_authors_join"."author_id" = "authors"."id"
    INNER JOIN "categories" "categories_categorizations" ON "categories_categorizations"."id" = "general_categorizations_authors_join"."category_id" AND "categories"."name" = ? -- <-- filtering `"categories"."name" = ?` won't work
    ---- `source: :post` part
    LEFT OUTER JOIN "posts" ON "posts"."id" = "general_categorizations_authors_join"."post_id"
    ```
    
    Originally eager loading with join scope didn't work before Rails 5.2
    (#29413), and duplicated through association with join scope raised a
    duplicated alias error before alias tracking is improved in 590b045e.
    
    But now it will potentially be got incorrect result instead of an error,
    it is worse than an error.
    
    To fix the issue, it makes eager loading to deduplicate / re-use
    duplicated through association if possible, like as `preload`.
    
    ```sql
    SELECT "authors"."id" AS t0_r0, ... FROM "authors"
    
    -- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
    LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
    INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
    
    -- `has_many :general_posts, through: :general_categorizations, source: :post`
    ---- `through: :general_categorizations` part is deduplicated / re-used
    LEFT OUTER JOIN "posts" ON "posts"."id" = "categorizations"."post_id"
    ```
    
    Fixes #32819.
Loading