require "../spec_helper" # TODO: add checking for log entries when we shouldn't hit db describe Jennifer::QueryBuilder::EagerLoading do describe "#with_relation" do it "adds to select clause given relation" do q1 = Contact.all.relation(:addresses) q1.with_relation(:addresses) select_clause(q1).should match(/SELECT #{reg_quote_identifier("contacts")}\.\*, #{reg_quote_identifier("addresses")}\.\*/) end it "raises error if given relation is not exists" do q1 = Contact.all expect_raises(Jennifer::UnknownRelation, "Unknown relation for Contact: relation") do q1.relation(:addresses).with_relation(:relation) select_clause(q1) end end it "raises error if given relation is not joined" do expect_raises(Jennifer::BaseException, /with_relation should be called after corresponding join/) do select_clause(Contact.all.with_relation(:addresses)) end end end describe "#eager_load" do it "allows to specify nested relation as a named tuple Symbol => Symbol alongside other top level argument" do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = Contact.all.eager_load(:addresses, :passport, countries: :cities).to_a expect_query_silence do res.size.should eq(1) res[0].addresses.size.should eq(1) res[0].passport.should be_nil # NOTE: passport doesn't exist and this doesn't breaks other loading res[0].countries.size.should eq(1) res[0].countries[0].cities.size.should eq(1) end end it "allows to specify nested relation as a named tuple Symbol => Array(Symbol) alongside other top level argument" do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = Contact.all.eager_load(:addresses, :passport, countries: [:cities]).to_a expect_query_silence do res.size.should eq(1) res[0].addresses.size.should eq(1) res[0].passport.should be_nil # NOTE: passport doesn't exist and this doesn't breaks other loading res[0].countries.size.should eq(1) res[0].countries[0].cities.size.should eq(1) end end it "allows to specify nested relation as a named tuple Symbol => Hash(String, Array(Symbol))" do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = City.all.eager_load(country: {:contacts => [:addresses, :passport]}).to_a expect_query_silence do res.size.should eq(1) res[0].country!.contacts.size.should eq(1) res[0].country!.contacts[0].addresses.size.should eq(1) res[0].country!.contacts[0].passport.should be_nil end end it "properly loads several relations from the same table" do c = Factory.create_contact Factory.create_address(contact_id: c.id, main: false) main_address = Factory.create_address(contact_id: c.id, main: true) res = Contact.all.eager_load(:addresses, :main_address).to_a expect_query_silence do res[0].addresses.size.should eq(2) res[0].main_address!.id.should eq(main_address.id) end end it "stops reloading relation from db if there is no records" do Factory.create_contact c = Contact.all.eager_load(:addresses).to_a expect_query_silence do c[0].addresses.should be_empty end end context "when record in specified relation chain doesn't exist" do it do country = Factory.create_country Factory.create_city(country_id: country.id) res = City.all.eager_load(country: {:contacts => [:addresses]}).to_a expect_query_silence do res.size.should eq(1) res[0].country!.contacts.should be_empty end end end context "when relation is defined with inverse_of option" do it "loads relation as well" do c1 = Factory.create_contact(name: "asd") Factory.create_address(contact_id: c1.id, street: "asd st.") res = Contact.all.eager_load(:addresses).first! expect_query_silence do res.addresses[0].street.should eq("asd st.") end end it "sets all deep relations" do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = City.all.eager_load(country: {:contacts => [:addresses]}).to_a expect_query_silence do res[0].country!.contacts[0].addresses[0].contact end end end context "when object belongs to several parent objects" do it do country = Factory.create_country Factory.create_city(name: "c1", country_id: country.id) Factory.create_city(name: "c2", country_id: country.id) res = City.all.eager_load(:country).to_a expect_query_silence do res.size.should eq(2) res[0].country!.id.should eq(country.id) res[0].country!.same?(res[1].country).should be_true end end end context "when target class defines not all fields and has non strict mapping" do it "loads both target class fields and included ones" do contacts = Factory.create_contact(3) ids = contacts.map(&.id) Factory.create_address(contact_id: contacts[0].id) res = ContactWithDependencies.all.eager_load(:addresses).where { _id.in(ids) }.order(id: :asc).to_a res.size.should eq(3) res[0].addresses.size.should eq(1) res[0].name.nil?.should be_false end end context "related model has own request" do it "respects additional request specified for a relation" do contact = Factory.create_contact Factory.create_address(contact_id: contact.id) main_address = Factory.create_address(contact_id: contact.id, main: true) Contact.all.eager_load(:main_address).first!.main_address!.id.should eq(main_address.id) end end end describe "#includes" do context "with nested relation defined as symbol" do it do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = Contact.all.includes(:addresses, :passport, countries: :cities).to_a expect_query_silence do res.size.should eq(1) res[0].addresses.size.should eq(1) res[0].passport.should be_nil res[0].countries.size.should eq(1) res[0].countries[0].cities.size.should eq(1) end end end context "with nested relation defined as array" do it do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = Contact.all.includes(:addresses, :passport, countries: [:cities]).to_a expect_query_silence do res.size.should eq(1) res[0].addresses.size.should eq(1) res[0].passport.should be_nil res[0].countries.size.should eq(1) res[0].countries[0].cities.size.should eq(1) end end end context "with nested relation defined as hash" do it do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = City.all.includes(country: {:contacts => [:addresses, :passport]}).to_a expect_query_silence do res.size.should eq(1) res[0].country!.contacts.size.should eq(1) res[0].country!.contacts[0].addresses.size.should eq(1) res[0].country!.contacts[0].passport.should be_nil end end end it "doesn't add JOIN condition" do Contact.all.includes(:addresses)._joins?.should be_falsey end it "stops reloading relation from db if there is no records" do Factory.create_contact c = Contact.all.includes(:addresses).to_a expect_query_silence do c[0].addresses end end it "stop reloading relation from the db if it is already loaded" do c = Factory.create_contact Factory.create_address(contact_id: c.id) c = Contact.all.includes(:addresses).to_a expect_query_silence do c[0].addresses.size.should eq(1) end end it "loads STI record" do c = ContactWithDependencies.create!({name: "test name", description: "description"}) profile1 = Factory.create_facebook_profile(contact_id: c.id) profile2 = Factory.create_twitter_profile(contact_id: c.id) ContactWithDependencies.eager_load(:profiles).order(Profile._id.asc).each do |contact| contact.id.should eq(c.id) contact.profiles.size.should eq(2) contact.profiles[0].should be_a(FacebookProfile) contact.profiles[0].as(FacebookProfile).uid.should eq(profile1.uid) contact.profiles[1].should be_a(TwitterProfile) contact.profiles[1].as(TwitterProfile).email.should eq(profile2.email) end end context "with defined inverse_of" do it "sets owner during building collection" do c = Factory.create_contact Factory.create_address(contact_id: c.id) res = Contact.all.includes(:addresses).to_a expect_query_silence do res[0].addresses[0].contact end end it "sets all deep relations" do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = City.all.includes(country: {:contacts => [:addresses]}).to_a expect_query_silence do res.size.should eq(1) res[0].country!.contacts[0].addresses[0].contact end end end describe "one-to-many relation" do it do c = Factory.create_contact Factory.create_address(contact_id: c.id) Factory.create_address(contact_id: c.id) executed_times = 0 Address.all.includes(:contact).to_a.each do |address| executed_times += 1 address.contact.should_not be_nil end executed_times.should eq(2) end end end describe "#preload" do # NOTE: #preload is an alias for #includes so just check if it delegates call to original method context "with nested relation defined as array" do it do c = Factory.create_contact(name: "contact 1") Factory.create_address(contact_id: c.id, street: "some st.") country = Factory.create_country Factory.create_city(country_id: country.id) c.add_countries(country) res = Contact.all.includes(:addresses, :passport, countries: [:cities]).to_a expect_query_silence do res.size.should eq(1) res[0].addresses.size.should eq(1) res[0].passport.should be_nil res[0].countries.size.should eq(1) res[0].countries[0].cities.size.should eq(1) end end end end describe "#_select_fields" do context "query has no specified select fields" do context "has eager loaded relations" do subject = Contact.all.eager_load(:addresses)._select_fields it "includes own star criteria" do subject.any? { |e| e.table == "contacts" && e.field == "*" }.should be_true end it "includes all mentioned relation star criterias" do subject.any? { |e| e.table == "addresses" && e.field == "*" }.should be_true end end it "returns only own star riteria" do fields = Contact.all._select_fields fields.size.should eq(1) fields[0].is_a?(Jennifer::QueryBuilder::Star).should be_true fields[0].table.should eq("contacts") end end context "query has specified fields" do it "returns specified fields" do fields = Contact.all.select { [_id, _age] }._select_fields fields.map(&.field).should eq(%w(id age)) end end end end