rails / rails

Ruby on Rails
https://rubyonrails.org
MIT License
56.06k stars 21.67k forks source link

Inverses are not properly set for belongs_to associations using foreign keys and scopes #47574

Open andrewberls opened 1 year ago

andrewberls commented 1 year ago

Steps to reproduce

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  # See note; bug was introduced in 6.1.0 and persists through HEAD
  gem "activerecord", "~> 6.1.7.2"
  gem "sqlite3"
end

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

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :authors, force: true do |t|
    t.string :token
  end

  create_table :posts, force: true do |t|
    t.string :author_token
    t.string :status
  end
end

class Author < ActiveRecord::Base
  has_one :post,
    -> { where(status: 'published') },
    primary_key: :token,
    foreign_key: :author_token,
    inverse_of: :author
end

class Post < ActiveRecord::Base
  belongs_to :author,
    primary_key: :token,
    foreign_key: :author_token
end

class BugTest < Minitest::Test
  def test_inversing
    author = Author.create!(token: 'A-123')
    Post.create!(author: author, status: 'published')

    assert_equal author.post.author.object_id, author.object_id
  end
end

I also have a skeleton repo demonstrating this issue here, with branches showing failure for versions >= 6.1.0.

Expected behavior

The Post#author association should be inversed, and author.post.author.object_id should exactly equal author.object_id

Actual behavior

The association does not inverse and the DB is queried to retrieve post.author.

I believe the bug was introduced in https://github.com/rails/rails/commit/c6f46564771413f4a06703fc6b1f82ed09a0c4c9 as part of 6.1.0, and exists through HEAD as the code has not changed. In this instance we can see are comparing the foreign key attribute (token) to the ID here, which clearly fails and thus the association is not properly considered inversable?.

Comparing instead to record.send(reflection.association_primary_key) may fix although I am not familiar with Rails internals and would need some assistance to submit a properly-scoped and tested PR.

The bug is NOT observed in the absence of a scope; specifically only the set_inverse_instance_from_queries pathway seems to demonstrate the bug.

System configuration

Rails version: The spec demonstrates 6.1.7.2 but fails beginning at 6.1.0 and persists through HEAD

Ruby version: 2.7.6 (bug also present on 3.0.x)

Thank you!

nickborromeo commented 1 year ago

I opened https://github.com/rails/rails/pull/47756 based on your report and suggested change 🙇 lets see if it gets accepted.