Skip to content

ActiveRecord: Missing joined table after upgrading to 6.1 #41498

@kaspernj

Description

@kaspernj

Steps to reproduce

I have written a test to reproduce the error. Passing on 6.0 and failing on 6.1.

Put this file in its own directory and run it from there.

# If you change the gem dependencies, run it with:
# `rm Gemfile* && ruby test.rb`

unless File.exist?("Gemfile")
  File.write("Gemfile", <<-GEMFILE)
    source "https://rubygems.org"
    # gem "rails", github: "rails/rails", branch: "6-0-stable"
    gem "rails", github: "rails/rails", branch: "6-1-stable"
    gem "pg"
  GEMFILE

  system "bundle install"
end

require "bundler"
Bundler.setup(:default)

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(
  adapter: "postgresql",
  database: "activerecord_test",
  host: "postgres",
  user: "username",
  password: "password"
)
# ActiveRecord::Base.logger = Logger.new(STDOUT)

# Display versions.
message = "Running test case with Ruby #{RUBY_VERSION}, Active Record #{
  ::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION} and #{
  ::ActiveRecord::Base.connection.adapter_name}"
line = "=" * message.length
puts line, message, line

ActiveRecord::Schema.define do
  create_table :contacts, force: true do |t|
    t.string :name
  end

  create_table :contact_relationships, force: true do |t|
    t.references :child
    t.references :parent
    t.string :relationship_type
  end
end

class Contact < ActiveRecord::Base
  has_one :commune_relationship, -> { where(relationship_type: "commune") }, class_name: "ContactRelationship", foreign_key: :child_id
  has_one :commune, source: :parent, through: :commune_relationship

  has_many :commune_for_relationships, -> { where(relationship_type: "commune") }, class_name: "ContactRelationship", foreign_key: :parent_id
  has_many :commune_for, source: :child, through: :commune_for_relationships

  has_many :schools_relationships, -> { where(relationship_type: "school") }, class_name: "ContactRelationship", foreign_key: :child_id
  has_many :schools, source: :parent, through: :schools_relationships

  has_many :schools_communes, source: :commune, through: :schools

  has_many :school_for_relationships, -> { where(relationship_type: "school") }, class_name: "ContactRelationship", foreign_key: :parent_id
  has_many :school_for, source: :child, through: :school_for_relationships

  has_many :children_relationships, class_name: "ContactRelationship", foreign_key: :parent_id
  has_many :children, source: :child, through: :children_relationships

  has_many :parents_relationships, class_name: "ContactRelationship", foreign_key: :child_id
  has_many :parents, source: :parent, through: :parents_relationships
end

class ContactRelationship < ActiveRecord::Base
  belongs_to :child, class_name: "Contact"
  belongs_to :parent, class_name: "Contact"
end

class TestActiveRecord < ActiveSupport::TestCase
  def setup
    @nothern_commune = Contact.create!(name: "Nothern Commune")
    @southern_commune = Contact.create!(name: "Southern Commune")

    @southern_school = Contact.create!(name: "Southern School")
    ContactRelationship.create!(child: @southern_school, parent: @southern_commune, relationship_type: "commune")

    @nothern_school = Contact.create!(name: "Nothern School")
    ContactRelationship.create!(child: @nothern_school, parent: @nothern_commune, relationship_type: "commune")

    # Student that both lives and goes to school in the north
    @nothern_student = Contact.create!(name: "Nothern Student")
    ContactRelationship.create!(child: @nothern_student, parent: @nothern_commune, relationship_type: "commune")
    ContactRelationship.create!(child: @nothern_student, parent: @nothern_school, relationship_type: "school")

    # Student that both lives and goes to school in the south
    @southern_student = Contact.create!(name: "Southern Student")
    ContactRelationship.create!(child: @southern_student, parent: @southern_commune, relationship_type: "commune")
    ContactRelationship.create!(child: @southern_student, parent: @southern_school, relationship_type: "school")

    # Student that lives in the northern commune but goes to school in the southern school
    @eastern_student = Contact.create!(name: "Eastern Student")
    ContactRelationship.create!(child: @eastern_student, parent: @nothern_commune, relationship_type: "commune")
    ContactRelationship.create!(child: @eastern_student, parent: @southern_school, relationship_type: "school")

    # Student that liaves in the southern commune but goes to school in the nothern school
    @western_student = Contact.create!(name: "Western Student")
    ContactRelationship.create!(child: @western_student, parent: @southern_commune, relationship_type: "commune")
    ContactRelationship.create!(child: @western_student, parent: @nothern_school, relationship_type: "school")
  end

  def test_setup
    assert_equal @nothern_commune, @nothern_student.commune
    assert_equal [@nothern_school], @nothern_student.schools
    assert_equal [@nothern_commune], @nothern_student.schools_communes

    assert_equal @southern_commune, @southern_student.commune
    assert_equal [@southern_school], @southern_student.schools
    assert_equal [@southern_commune], @southern_student.schools_communes

    assert_equal @nothern_commune, @eastern_student.commune
    assert_equal [@southern_school], @eastern_student.schools
    assert_equal [@southern_commune], @eastern_student.schools_communes

    assert_equal @southern_commune, @western_student.commune
    assert_equal [@nothern_school], @western_student.schools

    assert_equal [@nothern_school, @nothern_student, @eastern_student], @nothern_commune.children
    assert_equal [@nothern_school, @nothern_student, @eastern_student], @nothern_commune.commune_for

    assert_equal [@southern_school, @southern_student, @western_student], @southern_commune.children
    assert_equal [@southern_school, @southern_student, @western_student], @southern_commune.commune_for

    assert_equal [@nothern_student, @western_student], @nothern_school.children
    assert_equal [@nothern_student, @western_student], @nothern_school.school_for
    assert_equal [@southern_student, @eastern_student], @southern_school.school_for
  end

  def test_queries
    query = Contact
      .left_joins(:commune, schools: :commune)
      .where("communes_contacts.id = :commune_id OR communes_contacts_2.id = :commune_id", commune_id: @southern_commune.id)

    # Output from Rails 6.0
    # SELECT "contacts".*
    # FROM "contacts"
    # LEFT OUTER JOIN "contact_relationships" ON "contact_relationships"."relationship_type" = 'commune' AND "contact_relationships"."child_id" = "contacts"."id"
    # LEFT OUTER JOIN "contacts" "communes_contacts" ON "communes_contacts"."id" = "contact_relationships"."parent_id"
    # LEFT OUTER JOIN "contact_relationships" "schools_relationships_contacts_join" ON "schools_relationships_contacts_join"."relationship_type" = 'school' AND "schools_relationships_contacts_join"."child_id" = "contacts"."id"
    # LEFT OUTER JOIN "contacts" "schools_contacts" ON "schools_contacts"."id" = "schools_relationships_contacts_join"."parent_id"
    # Note: This relationships-join is missing in Rails 6.1
    # LEFT OUTER JOIN "contact_relationships" "commune_relationships_contacts_join" ON "commune_relationships_contacts_join"."relationship_type" = 'commune' AND "commune_relationships_contacts_join"."child_id" = "schools_contacts"."id"
    # LEFT OUTER JOIN "contacts" "communes_contacts_2" ON "communes_contacts_2"."id" = "commune_relationships_contacts_join"."parent_id"
    # WHERE (communes_contacts.id = 2 OR communes_contacts_2.id = 2)

    # Output from Rails 6.1
    # SELECT "contacts".*
    # FROM "contacts"
    # LEFT OUTER JOIN "contact_relationships" ON "contact_relationships"."relationship_type" = 'commune' AND "contact_relationships"."child_id" = "contacts"."id"
    # LEFT OUTER JOIN "contacts" "communes_contacts" ON "communes_contacts"."id" = "contact_relationships"."parent_id"
    # LEFT OUTER JOIN "contact_relationships" "schools_relationships_contacts_join" ON "schools_relationships_contacts_join"."relationship_type" = 'school' AND "schools_relationships_contacts_join"."child_id" = "contacts"."id"
    # LEFT OUTER JOIN "contacts" "schools_contacts" ON "schools_contacts"."id" = "schools_relationships_contacts_join"."parent_id"
    # Note: Missing relationships join like in Rails 6.0
    # LEFT OUTER JOIN "contacts" "communes_contacts_2" ON "communes_contacts_2"."id" = "contact_relationships"."parent_id"
    # WHERE (communes_contacts.id = 2 OR communes_contacts_2.id = 2)

    # puts "SQL:\n#{query.to_sql}"

    # Southern school should be included because it belongs to the southern commune
    # Southern student should be included because he both goes to school and lives in the south
    # Eastern student should be included because he goes to school in the southern school that belongs to the southern commune
    # Western student should be included because he lives in the south and is directly related to the southern commune

    assert_equal [@southern_school, @southern_student, @eastern_student, @western_student], query
  end
end

Expected behavior

I would expect the "contact_relationships" for communes on schools to be present instead of re-using the previous join wrongly.

I would expect this assertion to pass:

assert_equal [@southern_school, @southern_student, @eastern_student, @western_student], query

Actual behavior

It uses the previous join wrongly and reaches the wrong result.

System configuration

Rails version: 6.1

Ruby version: 2.6.6

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions