rails icon indicating copy to clipboard operation
rails copied to clipboard

association with `query_constraints: []` always returns empty collection when included

Open doits opened this issue 6 months ago • 3 comments

Steps to reproduce

With query_constraints: [] one can create a custom query for an association, for example:

class BodyPart < ActiveRecord::Base
end

class Human < ActiveRecord::Base
  has_many :body_parts, -> { where(for: 'human') }, query_constraints: []
end

This results in the following correct sql:

Human.last.body_parts.to_sql
# => SELECT "body_parts".* FROM "body_parts" WHERE "body_parts"."for" = 'human'

So far so good, but it always returns an empty collection when adding an includes(:body_parts):

Human.create!
BodyPart.create!(for: 'human')

Human.last.body_parts
# => [<instance of BodyPart>]
# correct

Human.includes(:body_parts).last.body_parts
# => []
# wrong

Is this a misuse of query_constraints? Should it break on query_constraints: []?

Or is this a bug including/eager_loading associations in case there are no query_constraints?

Background

The idea is to have custom associations that can be preloaded. The example above is very simple and doesn't depend on the the attributes at all (so the result is the same for every human), but it could be more complicated with a custom where query string etc. that takes into account the attributes of the model.

Test template

Template
# frozen_string_literal: true

require 'bundler/inline'

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

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

  gem 'rails', github: 'rails/rails', branch: 'main'
  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 :humen, id: :uuid, force: true do |t|
  end

  create_table :body_parts, id: :uuid, force: true do |t|
    t.string :for
  end
end

class BodyPart < ActiveRecord::Base
end

class Human < ActiveRecord::Base
  has_many :body_parts, -> { where(for: 'human') }, query_constraints: []
end

class BugTest < Minitest::Test
  def test_without_include
    Human.delete_all
    BodyPart.delete_all

    body_part_for_human = BodyPart.create!(id: SecureRandom.uuid, for: :human)
    BodyPart.create!(id: SecureRandom.uuid, for: :dog)

    Human.create!(id: SecureRandom.uuid)

    assert Human.all.first.body_parts == [body_part_for_human]
  end

  def test_with_include
    Human.delete_all
    BodyPart.delete_all

    body_part_for_human = BodyPart.create!(id: SecureRandom.uuid, for: :human)
    BodyPart.create!(id: SecureRandom.uuid, for: :dog)

    Human.create!(id: SecureRandom.uuid)

    assert Human.all.includes(:body_parts).first.body_parts == [body_part_for_human]
  end
end

References

might ref #50068

System configuration

Rails version: main

Ruby version: ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [arm64-darwin22]

doits avatar Nov 16 '23 12:11 doits