diff --git a/lib/mongoid/includes/inclusion.rb b/lib/mongoid/includes/inclusion.rb index 871b2ac..d61bff2 100644 --- a/lib/mongoid/includes/inclusion.rb +++ b/lib/mongoid/includes/inclusion.rb @@ -54,6 +54,7 @@ def load_documents_for(foreign_key, foreign_key_values) # Returns an Inclusion that can be eager loaded as usual. def for_class_name(class_name) Inclusion.new metadata.clone.instance_eval { |relation_metadata| + @options = @options.dup @options[:class_name] = @class_name = class_name @options[:polymorphic], @options[:as], @polymorphic, @klass = nil self diff --git a/spec/mongoid/includes/polymorphic_includes_spec.rb b/spec/mongoid/includes/polymorphic_includes_spec.rb index 3c933f7..8dba4e0 100644 --- a/spec/mongoid/includes/polymorphic_includes_spec.rb +++ b/spec/mongoid/includes/polymorphic_includes_spec.rb @@ -56,5 +56,117 @@ }.to raise_error(Mongoid::Includes::Errors::InvalidPolymorphicIncludes) end end + + context 'eager loading polymorphic belongs_to associations with multiple concrete types' do + before(:context) do + class Main + include Mongoid::Document + belongs_to :related, polymorphic: true, optional: true + end + + class Related + include Mongoid::Document + has_one :parent, as: :related + end + + class Two < Related; end + class Three < Related; end + end + + after(:context) do + %i[Main Related Two Three].each do |const| + Object.send(:remove_const, const) if Object.const_defined?(const, false) + end + end + + it 'loads the related documents for each concrete type without raising' do + Main.create!(related: Two.create!) + Main.create!(related: Three.create!) + + loaded = nil + expect { loaded = Main.includes(:related).entries }.not_to raise_error + expect(loaded.map { |doc| doc.related.class }).to match_array([Two, Three]) + expect { Main.last.related.id }.not_to raise_error + end + end + + context 'polymorphic eager loading in a fresh Ruby process' do + let(:project_root) { File.expand_path('../../..', __dir__) } + + it 'does not error when includes is evaluated from the CLI' do + require 'open3' + + database_name = "mongoid_includes_spec_#{SecureRandom.hex(6)}" + base_script = <<~RUBY + require 'bundler/setup' + require 'mongoid' + require 'mongoid_includes' + + Mongoid.load_configuration( + clients: { + default: { + database: '#{database_name}', + hosts: %w[localhost:27017] + } + } + ) + + class Main + include Mongoid::Document + belongs_to :related, polymorphic: true, optional: true + end + + class Related + include Mongoid::Document + has_one :parent, as: :related + end + + class Two < Related; end + class Three < Related; end + RUBY + + init_script = base_script + <<~RUBY + client = Mongoid::Clients.default + begin + client.database.drop + rescue Mongo::Error::OperationFailure + end + + Main.destroy_all + Related.destroy_all + + Main.create!(related: Two.create!) + Main.create!(related: Three.create!) + RUBY + + bad_script = base_script + <<~RUBY + Main.includes(:related).entries + Main.last.related.id + + Mongoid::Clients.default.database.drop + RUBY + + bundle_gemfile = ENV.fetch( + 'BUNDLE_GEMFILE', + File.join(project_root, 'Gemfile') + ) + + run_script = lambda do |script| + Open3.capture2e( + { 'BUNDLE_GEMFILE' => bundle_gemfile }, + RbConfig.ruby, + '-', + chdir: project_root, + stdin_data: script + ) + end + + init_out, init_status = run_script.call(init_script) + expect(init_status).to be_success, "failed to prepare polymorphic data: #{init_out}" + + bad_out, bad_status = run_script.call(bad_script) + expect(bad_status).to be_success, "expected CLI reproduction to succeed, got #{bad_status.exitstatus}: #{bad_out}" + end + end end end