helper to create a complete tree normally accessing fks doesn t fill in related objects a selectrelated call will fill in those related objects without any extra queries selectrelated applies to entire lists not just items with self assertnumqueries1 world species objects selectrelated families o genus family name for o in world self assertequal sortedfamilies amanitacae drosophilidae fabaceae hominidae def testlistwithdepthself with self assertnumqueries5 world species objects selectrelatedgenusfamily orders o genus family order name for o in world self assertequal sortedorders agaricales diptera fabales primates def testselectrelatedwithextraself s species objects all selectrelated extraselecta selectrelatedspecies id 100 self assertequals id 10 s a def testcertainfieldsself with self assertnumqueries1 world species objects selectrelatedgenusfamily families o genus family name for o in world self assertequal sortedfamilies amanitacae drosophilidae fabaceae hominidae def testmorecertainfieldsself with self assertnumqueries2 world species objects filtergenusnameamanita selectrelated genusfamily orders o genus family order name for o in world self assertequalorders agaricales def testfieldtraversalself with self assertnumqueries1 s species objects all selectrelatedgenusfamilyorder orderbyid0 1 get genus family order name self assertequals diptera def testnoneclearslistself queryset species objects selectrelatedgenus selectrelatednone self assertisqueryset query selectrelated false def testchainingself parent1 parent2 species objects all 2 hybridspecies objects create namehybrid parent1parent1 parent2parent2 queryset hybridspecies objects selectrelatedparent1 selectrelated parent2 with self assertnumqueries1 obj queryset0 self assertequalobj parent1 parent1 self assertequalobj parent2 parent2 def testreverserelationcachingself species species objects selectrelatedgenus filternamemelanogaster first with self assertnumqueries0 self assertequalspecies genus name drosophila the speciesset reverse relation isn t cached self assertequalspecies genus state fieldscache with self assertnumqueries1 self assertequalspecies genus speciesset first name melanogaster def testselectrelatedaftervaluesself message cannot call selectrelated after values or valueslist with self assertraisesmessagetypeerror message listspecies objects valuesname selectrelatedgenus def testselectrelatedaftervalueslistself message cannot call selectrelated after values or valueslist with self assertraisesmessagetypeerror message listspecies objects valueslistname selectrelatedgenus class selectrelatedvalidationtestssimpletestcase nonrelationalerror nonrelational field given in selectrelated s choices are s invaliderror invalid field names given in selectrelated s choices are s def testnonrelationalfieldself with self assertraisesmessage fielderror self nonrelationalerror name genus listspecies objects selectrelatednamesomefield with self assertraisesmessage fielderror self nonrelationalerror name genus listspecies objects selectrelatedname with self assertraisesmessage fielderror self nonrelationalerror name none listdomain objects selectrelatedname def testnonrelationalfieldnestedself with self assertraisesmessage fielderror self nonrelationalerror name family listspecies objects selectrelatedgenusname def testmanytomanyfieldself with self assertraisesmessage fielderror self invaliderror toppings none listpizza objects selectrelatedtoppings def testreverserelationalfieldself with self assertraisesmessage fielderror self invaliderror child1 genus listspecies objects selectrelatedchild1 def testinvalidfieldself with self assertraisesmessage fielderror self invaliderror invalidfield genus listspecies objects selectrelatedinvalidfield with self assertraisesmessage fielderror self invaliderror relatedinvalidfield family listspecies objects selectrelatedgenusrelatedinvalidfield with self assertraisesmessage fielderror self invaliderror invalidfield none listdomain objects selectrelatedinvalidfield def testgenericrelationsself with self assertraisesmessagefielderror self invaliderror tags listbookmark objects selectrelatedtags with self assertraisesmessage fielderror self invaliderror contentobject contenttype listtaggeditem objects selectrelatedcontentobject helper to create a complete tree normally accessing fks doesn t fill in related objects a select_related call will fill in those related objects without any extra queries select_related applies to entire lists not just items passing a relationship field lookup specifier to select_related will stop the descent at a particular level this can be used on lists as well the optional fields passed to select_related control which related models we pull in this allows for smaller queries in this case we explicitly say to select the genus and genus family models leading to the same number of queries as before in this case we explicitly say to select the genus and genus family models leading to the same number of queries as before the species_set reverse relation isn t cached running select_related after calling values raises a typeerror running select_related after calling values_list raises a typeerror select_related should thrown an error on fields that do not exist and non relational fields
from django.core.exceptions import FieldError
from django.test import SimpleTestCase, TestCase

from .models import (
    Bookmark,
    Domain,
    Family,
    Genus,
    HybridSpecies,
    Kingdom,
    Klass,
    Order,
    Phylum,
    Pizza,
    Species,
    TaggedItem,
)


class SelectRelatedTests(TestCase):
    @classmethod
    def create_tree(cls, stringtree):
        
        names = stringtree.split()
        models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species]
        assert len(names) == len(models), (names, models)

        parent = None
        for name, model in zip(names, models):
            try:
                obj = model.objects.get(name=name)
            except model.DoesNotExist:
                obj = model(name=name)
            if parent:
                setattr(obj, parent.__class__.__name__.lower(), parent)
            obj.save()
            parent = obj

    @classmethod
    def setUpTestData(cls):
        cls.create_tree(
            "Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila "
            "melanogaster"
        )
        cls.create_tree(
            "Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens"
        )
        cls.create_tree(
            "Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum "
            "sativum"
        )
        cls.create_tree(
            "Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae "
            "Amanita muscaria"
        )

    def test_access_fks_without_select_related(self):
        
        with self.assertNumQueries(8):
            fly = Species.objects.get(name="melanogaster")
            domain = fly.genus.family.order.klass.phylum.kingdom.domain
            self.assertEqual(domain.name, "Eukaryota")

    def test_access_fks_with_select_related(self):
        
        with self.assertNumQueries(1):
            person = Species.objects.select_related(
                "genus__family__order__klass__phylum__kingdom__domain"
            ).get(name="sapiens")
            domain = person.genus.family.order.klass.phylum.kingdom.domain
            self.assertEqual(domain.name, "Eukaryota")

    def test_list_without_select_related(self):
        with self.assertNumQueries(9):
            world = Species.objects.all()
            families = [o.genus.family.name for o in world]
            self.assertEqual(
                sorted(families),
                [
                    "Amanitacae",
                    "Drosophilidae",
                    "Fabaceae",
                    "Hominidae",
                ],
            )

    def test_list_with_select_related(self):
        
        with self.assertNumQueries(1):
            world = Species.objects.select_related()
            families = [o.genus.family.name for o in world]
            self.assertEqual(
                sorted(families),
                [
                    "Amanitacae",
                    "Drosophilidae",
                    "Fabaceae",
                    "Hominidae",
                ],
            )

    def test_list_with_depth(self):
        
        with self.assertNumQueries(5):
            world = Species.objects.select_related("genus__family")
            orders = [o.genus.family.order.name for o in world]
            self.assertEqual(
                sorted(orders), ["Agaricales", "Diptera", "Fabales", "Primates"]
            )

    def test_select_related_with_extra(self):
        s = (
            Species.objects.all()
            .select_related()
            .extra(select={"a": "select_related_species.id + 10"})[0]
        )
        self.assertEqual(s.id + 10, s.a)

    def test_certain_fields(self):
        
        with self.assertNumQueries(1):
            world = Species.objects.select_related("genus__family")
            families = [o.genus.family.name for o in world]
            self.assertEqual(
                sorted(families),
                ["Amanitacae", "Drosophilidae", "Fabaceae", "Hominidae"],
            )

    def test_more_certain_fields(self):
        
        with self.assertNumQueries(2):
            world = Species.objects.filter(genus__name="Amanita").select_related(
                "genus__family"
            )
            orders = [o.genus.family.order.name for o in world]
            self.assertEqual(orders, ["Agaricales"])

    def test_field_traversal(self):
        with self.assertNumQueries(1):
            s = (
                Species.objects.all()
                .select_related("genus__family__order")
                .order_by("id")[0:1]
                .get()
                .genus.family.order.name
            )
            self.assertEqual(s, "Diptera")

    def test_none_clears_list(self):
        queryset = Species.objects.select_related("genus").select_related(None)
        self.assertIs(queryset.query.select_related, False)

    def test_chaining(self):
        parent_1, parent_2 = Species.objects.all()[:2]
        HybridSpecies.objects.create(
            name="hybrid", parent_1=parent_1, parent_2=parent_2
        )
        queryset = HybridSpecies.objects.select_related("parent_1").select_related(
            "parent_2"
        )
        with self.assertNumQueries(1):
            obj = queryset[0]
            self.assertEqual(obj.parent_1, parent_1)
            self.assertEqual(obj.parent_2, parent_2)

    def test_reverse_relation_caching(self):
        species = (
            Species.objects.select_related("genus").filter(name="melanogaster").first()
        )
        with self.assertNumQueries(0):
            self.assertEqual(species.genus.name, "Drosophila")
        
        self.assertEqual(species.genus._state.fields_cache, {})
        with self.assertNumQueries(1):
            self.assertEqual(species.genus.species_set.first().name, "melanogaster")

    def test_select_related_after_values(self):
        
        message = "Cannot call select_related() after .values() or .values_list()"
        with self.assertRaisesMessage(TypeError, message):
            list(Species.objects.values("name").select_related("genus"))

    def test_select_related_after_values_list(self):
        
        message = "Cannot call select_related() after .values() or .values_list()"
        with self.assertRaisesMessage(TypeError, message):
            list(Species.objects.values_list("name").select_related("genus"))


class SelectRelatedValidationTests(SimpleTestCase):
    

    non_relational_error = (
        "Non-relational field given in select_related: '%s'. Choices are: %s"
    )
    invalid_error = (
        "Invalid field name(s) given in select_related: '%s'. Choices are: %s"
    )

    def test_non_relational_field(self):
        with self.assertRaisesMessage(
            FieldError, self.non_relational_error % ("name", "genus")
        ):
            list(Species.objects.select_related("name__some_field"))

        with self.assertRaisesMessage(
            FieldError, self.non_relational_error % ("name", "genus")
        ):
            list(Species.objects.select_related("name"))

        with self.assertRaisesMessage(
            FieldError, self.non_relational_error % ("name", "(none)")
        ):
            list(Domain.objects.select_related("name"))

    def test_non_relational_field_nested(self):
        with self.assertRaisesMessage(
            FieldError, self.non_relational_error % ("name", "family")
        ):
            list(Species.objects.select_related("genus__name"))

    def test_many_to_many_field(self):
        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("toppings", "(none)")
        ):
            list(Pizza.objects.select_related("toppings"))

    def test_reverse_relational_field(self):
        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("child_1", "genus")
        ):
            list(Species.objects.select_related("child_1"))

    def test_invalid_field(self):
        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("invalid_field", "genus")
        ):
            list(Species.objects.select_related("invalid_field"))

        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("related_invalid_field", "family")
        ):
            list(Species.objects.select_related("genus__related_invalid_field"))

        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("invalid_field", "(none)")
        ):
            list(Domain.objects.select_related("invalid_field"))

    def test_generic_relations(self):
        with self.assertRaisesMessage(FieldError, self.invalid_error % ("tags", "")):
            list(Bookmark.objects.select_related("tags"))

        with self.assertRaisesMessage(
            FieldError, self.invalid_error % ("content_object", "content_type")
        ):
            list(TaggedItem.objects.select_related("content_object"))