require "spec_helper" require "db-query-matchers" RSpec.describe JitPreloader::Preloader do let!(:contact1) do addresses = [ Address.new(street: "123 Fake st", country: canada), Address.new(street: "21 Jump st", country: usa), Address.new(street: "90210 Beverly Hills", country: usa) ] phones = [ PhoneNumber.new(phone: "4445556666"), PhoneNumber.new(phone: "2223333444") ] Contact.create(name: "Only Addresses", addresses: addresses, phone_numbers: phones) end let!(:contact2) do Contact.create(name: "Only Emails", email_address: EmailAddress.new(address: "woot@woot.com")) end let!(:contact3) do addresses = [ Address.new(street: "1 First st", country: canada), Address.new(street: "10 Tenth Ave", country: usa) ] Contact.create( name: "Both!", addresses: addresses, email_address: EmailAddress.new(address: "woot@woot.com"), phone_numbers: [PhoneNumber.new(phone: "1234567890")] ) end let!(:contact_owner) do contact3.contact_owner_id = contact1.id contact3.contact_owner_type = "Address" contact3.save! ContactOwner.create( contacts: [contact1, contact2], ) end let(:canada) { Country.create(name: "Canada") } let(:usa) { Country.create(name: "U.S.A") } let(:source_map) { Hash.new{|h,k| h[k]= Array.new } } let(:callback) do ->(event, data){ source_map[data[:source]] << data[:association] } end context "for single table inheritance" do context "when preloading an aggregate for a child model" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) } let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) } it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.companies_count).to eq 2 end end context "when preloading an aggregate of a child model through its base model" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) } let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) } let!(:contact_employee1) { Employee.create(name: "Contact Employee1", contact: contact) } let!(:contact_employee2) { Employee.create(name: "Contact Employee2", contact: contact) } let!(:company_employee1) { Employee.create(name: "Company Employee1", contact: company1) } let!(:company_employee2) { Employee.create(name: "Company Employee2", contact: company2) } it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.employees_count).to eq 4 end end context "when preloading an aggregate of a nested child model through another child model" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) } let!(:company2) { Company.create(name: "Company2", contact_book: contact_book) } let!(:contact_employee1) { Employee.create(name: "Contact Employee1", contact: contact) } let!(:contact_employee2) { Employee.create(name: "Contact Employee2", contact: contact) } let!(:company_employee1) { Employee.create(name: "Company Employee1", contact: company1) } let!(:company_employee2) { Employee.create(name: "Company Employee2", contact: company2) } it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.company_employees_count).to eq 2 end end context "when preloading an aggregate of a nested child model through a many-to-many relationship with another child model" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:child1) { Child.create(name: "Child1") } let!(:child2) { Child.create(name: "Child2") } let!(:child3) { Child.create(name: "Child3") } let!(:parent1) { Parent.create(name: "Parent1", contact_book: contact_book, children: [child1, child2]) } let!(:parent2) { Parent.create(name: "Parent2", contact_book: contact_book, children: [child2, child3]) } it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.children_count).to eq 4 expect(contact_books.first.children).to include(child1, child2, child3) end end context "when preloading an aggregate for a child model scoped by another join table" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:contact1) { Company.create(name: "Without Email", contact_book: contact_book) } let!(:contact2) { Company.create(name: "With Blank Email", email_address: EmailAddress.new(address: ""), contact_book: contact_book) } let!(:contact3) { Company.create(name: "With Email", email_address: EmailAddress.new(address: "a@a.com"), contact_book: contact_book) } it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.companies_with_blank_email_address_count).to eq 1 expect(contact_books.first.companies_with_blank_email_address).to eq [contact2] end end end context "when preloading an aggregate as polymorphic" do let(:contact_owner_counts) { [2] } context "without jit preload" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do ContactOwner.all.each_with_index do |c, i| expect(c.contacts_count).to eql contact_owner_counts[i] end end contact_owner_queries = [contact_owner].product([["contacts.count"]]) expect(source_map).to eql(Hash[contact_owner_queries]) end end context "with jit_preload" do it "does NOT generate N+1 query notifications" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do ContactOwner.jit_preload.each_with_index do |c, i| expect(c.contacts_count).to eql contact_owner_counts[i] end end expect(source_map).to eql({}) end it "can handle queries" do ContactOwner.jit_preload.each_with_index do |c, i| expect(c.contacts_count).to eql contact_owner_counts[i] end end context "when a record has a polymorphic association type that's not an ActiveRecord" do before do contact1.update!(contact_owner_type: "NilClass", contact_owner_id: nil) end it "doesn't die while trying to load the association" do expect(Contact.jit_preload.map(&:contact_owner)).to eq [nil, ContactOwner.first, Address.first] end end context "when a record has a polymorphic association type is nil" do before do contact1.update!(contact_owner_type: nil, contact_owner_id: nil) end it "successfully load the rest of association values and does not publish a n+1 notification" do contacts = Contact.jit_preload.to_a ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do expect(contacts.first.contact_owner).to eq(nil) end expect(source_map).to eql({}) expect do contacts.first.contact_owner contacts.second.contact_owner contacts.third.contact_owner end.not_to make_database_queries expect(contacts.second.contact_owner).to eq(ContactOwner.first) expect(contacts.third.contact_owner).to eq(Address.first) end end end end context "when preloading an aggregate on a has_many through relationship" do let(:country_contacts_counts) { [2, 3] } context "without jit preload" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Country.all.each_with_index do |c, i| expect(c.contacts_count).to eql country_contacts_counts[i] end end country_contact_queries = [canada, usa].product([["contacts.count"]]) expect(source_map).to eql(Hash[country_contact_queries]) end end context "with jit_preload" do it "does NOT generate N+1 query notifications" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Country.all.jit_preload.each_with_index do |c, i| expect(c.contacts_count).to eql country_contacts_counts[i] end end expect(source_map).to eql({}) end it "can handle queries" do Country.all.jit_preload.each_with_index do |c, i| expect(c.contacts_count).to eql country_contacts_counts[i] end end end end context "when accessing an association with a scope that has a parameter" do let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") } let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) } it "is unable to be preloaded" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do ContactBook.all.jit_preload.each do |contact_book| expect(contact_book.contacts_with_scope.to_a).to eql [company1, contact] end end expect(source_map).to eql(Hash[contact_book, [:contacts_with_scope]]) end end context "when preloading an aggregate on a polymorphic has_many through relationship" do let(:contact_owner_addresses_counts) { [3] } context "without jit preload" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do ContactOwner.all.each_with_index do |c, i| expect(c.addresses_count).to eql contact_owner_addresses_counts[i] end end contact_owner_addresses_queries = [contact_owner].product([["addresses.count"]]) expect(source_map).to eql(Hash[contact_owner_addresses_queries]) end end context "with jit_preload" do it "does NOT generate N+1 query notifications" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do ContactOwner.all.jit_preload.each_with_index do |c, i| expect(c.addresses_count).to eql contact_owner_addresses_counts[i] end end expect(source_map).to eql({}) end it "can handle queries" do ContactOwner.all.jit_preload.each_with_index do |c, i| expect(c.addresses_count).to eql contact_owner_addresses_counts[i] end end end end context "when preloading a has_many through polymorphic aggregate where the through class has a polymorphic relationship to the target class" do let(:contact_owner_counts) { [1, 2] } context "without jit preload" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Country.all.each_with_index do |c, i| expect(c.contact_owners_count).to eql contact_owner_counts[i] end end contact_owner_queries = [canada, usa].product([["contact_owners.count"]]) expect(source_map).to eql(Hash[contact_owner_queries]) end end context "with jit_preload" do it "does NOT generate N+1 query notifications" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Country.all.jit_preload.each_with_index do |c, i| expect(c.contact_owners_count).to eql contact_owner_counts[i] end end expect(source_map).to eql({}) end it "can handle queries" do Country.all.jit_preload.each_with_index do |c, i| expect(c.contact_owners_count).to eql contact_owner_counts[i] end end end end context "when a singular association id changes after preload" do let!(:contact_book1) { ContactBook.create(name: "The Yellow Pages") } let!(:contact_book2) { ContactBook.create(name: "The White Pages") } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book1) } let!(:company2) { Company.create(name: "Company2", contact_book: contact_book1) } it "allows the association to be reloaded" do companies = Company.where(id: [company1.id, company2.id]).jit_preload.all.to_a expect(companies.map(&:contact_book)).to match_array [contact_book1, contact_book1] company = companies.each {|c| c.contact_book_id = contact_book2.id } expect(companies.map(&:contact_book)).to match_array [contact_book2, contact_book2] end end context "when preloading an aggregate" do let(:addresses_counts) { [3, 0, 2] } let(:phone_number_counts) { [2, 0, 1] } let(:maxes) { [19, 0, 12] } context "without jit_preload" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.all.each_with_index do |c, i| expect(c.addresses_count).to eql addresses_counts[i] expect(c.addresses_max_street_length).to eql maxes[i] expect(c.phone_numbers_count).to eql phone_number_counts[i] end end contact_queries = [contact1, contact2, contact3].product([["addresses.count", "addresses.maximum", "phone_numbers.count"]]) expect(source_map).to eql(Hash[contact_queries]) end end context "with jit_preload" do let(:usa_addresses_counts) { [2, 0, 1] } let(:can_addresses_counts) { [1, 0, 1] } it "does NOT generate N+1 query notifications" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.jit_preload.each_with_index do |c, i| expect(c.addresses_count).to eql addresses_counts[i] expect(c.addresses_max_street_length).to eql maxes[i] expect(c.phone_numbers_count).to eql phone_number_counts[i] end end expect(source_map).to eql({}) end it "can handle dynamic queries" do Contact.jit_preload.each_with_index do |c, i| expect(c.addresses_count(country: usa)).to eql usa_addresses_counts[i] expect(c.addresses_count(country: canada)).to eql can_addresses_counts[i] end end end end context "when we marshal dump the active record object" do it "nullifes the jit_preloader reference" do contacts = Contact.jit_preload.to_a reloaded_contacts = contacts.collect{|r| Marshal.load(Marshal.dump(r)) } contacts.each do |c| expect(c.jit_preloader).to_not be_nil end reloaded_contacts.each do |c| expect(c.jit_preloader).to be_nil end end end context "when the preloader is globally enabled" do around do |example| JitPreloader.globally_enabled = true example.run JitPreloader.globally_enabled = false end it "doesn't reference the same records array that is returned" do contacts = Contact.all.to_a contacts << "A string" expect(contacts.first.jit_preloader.records).to eql Contact.all.to_a end context "when grabbing all of the address'es contries and email addresses" do it "doesn't generate an N+1 query ntoification" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.all.collect{|c| c.addresses.collect(&:country); c.email_address } end expect(source_map).to eql({}) end end context "when we perform aggregate functions on the data" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.all.each{|c| c.addresses.count; c.addresses.sum(:id) } end contact_queries = [contact1,contact2, contact3].product([["addresses.count", "addresses.sum"]]) expect(source_map).to eql(Hash[contact_queries]) end end end context "when the preloader is not globally enabled" do context "when we perform aggregate functions on the data" do it "generates N+1 query notifications for each one" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.all.each{|c| c.addresses.count; c.addresses.sum(:id) } end contact_queries = [contact1,contact2, contact3].product([["addresses.count", "addresses.sum"]]) expect(source_map).to eql(Hash[contact_queries]) end end context "when explicitly finding a contact" do it "generates N+1 query notifications for the country" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.find(contact1.id).tap{|c| c.addresses.collect(&:country); c.email_address } end address_queries = Address.where(contact_id: 1).to_a.product([[:country]]) expect(source_map).to eql(Hash[address_queries]) end end context "when explicitly finding multiple contacts" do it "generates N+1 query notifications for the country" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.find(contact1.id, contact2.id).each{|c| c.addresses.collect(&:country); c.email_address } end contact_queries = [contact1,contact2].product([[:addresses, :email_address]]) address_queries = Address.where(contact_id: contact1.id).to_a.product([[:country]]) expect(source_map).to eql(Hash[address_queries.concat(contact_queries)]) end end context "when grabbing the email address and address's country of the first contact" do it "generates N+1 query notifications for the country" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.first.tap{|c| c.addresses.collect(&:country); c.email_address } end address_queries = Address.where(contact_id: contact1.id).to_a.product([[:country]]) expect(source_map).to eql(Hash[address_queries]) end end context "when grabbing all of the address'es contries and email addresses" do it "generates an N+1 query for each association on the contacts" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.all.each{|c| c.addresses.collect(&:country); c.email_address } end contact_queries = [contact1,contact2,contact3].product([[:addresses, :email_address]]) address_queries = Address.all.to_a.product([[:country]]) expect(source_map).to eql(Hash[address_queries.concat(contact_queries)]) end context "and we use regular preload for addresses" do it "generates an N+1 query for only the email addresses on the contacts" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.preload(:addresses).each{|c| c.addresses.collect(&:country); c.email_address } end contact_queries = [contact1,contact2,contact3].product([[:email_address]]) address_queries = Address.all.to_a.product([[:country]]) expect(source_map).to eql(Hash[address_queries.concat(contact_queries)]) end end context "and we use jit preload" do it "generates no n+1 queries" do ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do Contact.jit_preload.each{|c| c.addresses.collect(&:country); c.email_address } end expect(source_map).to eql({}) end end context "reload" do it "clears the jit_preload_aggregates" do contact = Contact.jit_preload.first contact.addresses_count expect { contact.reload }.to change{ contact.jit_preload_aggregates }.to({}) end end end context "with dive limit set" do let!(:contact_book_1) { ContactBook.create(name: "The Yellow Pages") } let!(:contact_book_2) { ContactBook.create(name: "The Yellow Pages") } let!(:contact_book_3) { ContactBook.create(name: "The Yellow Pages") } let!(:company1) { Company.create(name: "Company1", contact_book: contact_book_1) } let!(:company2) { Company.create(name: "Company2", contact_book: contact_book_1) } let!(:company3) { Company.create(name: "Company2", contact_book: contact_book_2) } let!(:company4) { Company.create(name: "Company4", contact_book: contact_book_3) } let!(:company5) { Company.create(name: "Company5", contact_book: contact_book_3) } context "from the global value" do before do JitPreloader.max_ids_per_query = 2 end after do JitPreloader.max_ids_per_query = nil end it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.companies_count).to eq 2 expect(contact_books.second.companies_count).to eq 1 expect(contact_books.last.companies_count).to eq 2 end it "makes the right number of queries based on dive limit" do contact_books = ContactBook.jit_preload.to_a expect do contact_books.first.companies_count end.to make_database_queries(count: 2) expect do contact_books.second.companies_count contact_books.last.companies_count end.to_not make_database_queries end end context "from aggregate argument" do it "can handle queries" do contact_books = ContactBook.jit_preload.to_a expect(contact_books.first.companies_count_with_max_ids_set).to eq 2 expect(contact_books.second.companies_count_with_max_ids_set).to eq 1 expect(contact_books.last.companies_count_with_max_ids_set).to eq 2 end it "makes the right number of queries based on dive limit" do contact_books = ContactBook.jit_preload.to_a expect do contact_books.first.companies_count_with_max_ids_set end.to make_database_queries(count: 2) expect do contact_books.second.companies_count_with_max_ids_set contact_books.last.companies_count_with_max_ids_set end.to_not make_database_queries end end end end end