tests.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. from datetime import date
  2. from decimal import Decimal
  3. from unittest import mock
  4. from django.db import connection, transaction
  5. from django.db.models import (
  6. Case,
  7. Count,
  8. DecimalField,
  9. F,
  10. FilteredRelation,
  11. Q,
  12. Sum,
  13. Value,
  14. When,
  15. )
  16. from django.db.models.functions import Concat
  17. from django.test import TestCase
  18. from django.test.testcases import skipUnlessDBFeature
  19. from .models import (
  20. Author,
  21. Book,
  22. BookDailySales,
  23. Borrower,
  24. Currency,
  25. Editor,
  26. ExchangeRate,
  27. RentalSession,
  28. Reservation,
  29. Seller,
  30. )
  31. class FilteredRelationTests(TestCase):
  32. @classmethod
  33. def setUpTestData(cls):
  34. cls.author1 = Author.objects.create(name="Alice")
  35. cls.author2 = Author.objects.create(name="Jane")
  36. cls.editor_a = Editor.objects.create(name="a")
  37. cls.editor_b = Editor.objects.create(name="b")
  38. cls.book1 = Book.objects.create(
  39. title="Poem by Alice",
  40. editor=cls.editor_a,
  41. author=cls.author1,
  42. )
  43. cls.book1.generic_author.set([cls.author2])
  44. cls.book2 = Book.objects.create(
  45. title="The book by Jane A",
  46. editor=cls.editor_b,
  47. author=cls.author2,
  48. )
  49. cls.book3 = Book.objects.create(
  50. title="The book by Jane B",
  51. editor=cls.editor_b,
  52. author=cls.author2,
  53. )
  54. cls.book4 = Book.objects.create(
  55. title="The book by Alice",
  56. editor=cls.editor_a,
  57. author=cls.author1,
  58. )
  59. cls.author1.favorite_books.add(cls.book2)
  60. cls.author1.favorite_books.add(cls.book3)
  61. def test_select_related(self):
  62. qs = (
  63. Author.objects.annotate(
  64. book_join=FilteredRelation("book"),
  65. )
  66. .select_related("book_join__editor")
  67. .order_by("pk", "book_join__pk")
  68. )
  69. with self.assertNumQueries(1):
  70. self.assertQuerySetEqual(
  71. qs,
  72. [
  73. (self.author1, self.book1, self.editor_a, self.author1),
  74. (self.author1, self.book4, self.editor_a, self.author1),
  75. (self.author2, self.book2, self.editor_b, self.author2),
  76. (self.author2, self.book3, self.editor_b, self.author2),
  77. ],
  78. lambda x: (x, x.book_join, x.book_join.editor, x.book_join.author),
  79. )
  80. def test_select_related_multiple(self):
  81. qs = (
  82. Book.objects.annotate(
  83. author_join=FilteredRelation("author"),
  84. editor_join=FilteredRelation("editor"),
  85. )
  86. .select_related("author_join", "editor_join")
  87. .order_by("pk")
  88. )
  89. self.assertQuerySetEqual(
  90. qs,
  91. [
  92. (self.book1, self.author1, self.editor_a),
  93. (self.book2, self.author2, self.editor_b),
  94. (self.book3, self.author2, self.editor_b),
  95. (self.book4, self.author1, self.editor_a),
  96. ],
  97. lambda x: (x, x.author_join, x.editor_join),
  98. )
  99. def test_select_related_with_empty_relation(self):
  100. qs = (
  101. Author.objects.annotate(
  102. book_join=FilteredRelation("book", condition=Q(pk=-1)),
  103. )
  104. .select_related("book_join")
  105. .order_by("pk")
  106. )
  107. self.assertSequenceEqual(qs, [self.author1, self.author2])
  108. def test_select_related_foreign_key(self):
  109. qs = (
  110. Book.objects.annotate(
  111. author_join=FilteredRelation("author"),
  112. )
  113. .select_related("author_join")
  114. .order_by("pk")
  115. )
  116. with self.assertNumQueries(1):
  117. self.assertQuerySetEqual(
  118. qs,
  119. [
  120. (self.book1, self.author1),
  121. (self.book2, self.author2),
  122. (self.book3, self.author2),
  123. (self.book4, self.author1),
  124. ],
  125. lambda x: (x, x.author_join),
  126. )
  127. @skipUnlessDBFeature("has_select_for_update", "has_select_for_update_of")
  128. def test_select_related_foreign_key_for_update_of(self):
  129. with transaction.atomic():
  130. qs = (
  131. Book.objects.annotate(
  132. author_join=FilteredRelation("author"),
  133. )
  134. .select_related("author_join")
  135. .select_for_update(of=("self",))
  136. .order_by("pk")
  137. )
  138. with self.assertNumQueries(1):
  139. self.assertQuerySetEqual(
  140. qs,
  141. [
  142. (self.book1, self.author1),
  143. (self.book2, self.author2),
  144. (self.book3, self.author2),
  145. (self.book4, self.author1),
  146. ],
  147. lambda x: (x, x.author_join),
  148. )
  149. def test_without_join(self):
  150. self.assertCountEqual(
  151. Author.objects.annotate(
  152. book_alice=FilteredRelation(
  153. "book", condition=Q(book__title__iexact="poem by alice")
  154. ),
  155. ),
  156. [self.author1, self.author2],
  157. )
  158. def test_with_join(self):
  159. self.assertSequenceEqual(
  160. Author.objects.annotate(
  161. book_alice=FilteredRelation(
  162. "book", condition=Q(book__title__iexact="poem by alice")
  163. ),
  164. ).filter(book_alice__isnull=False),
  165. [self.author1],
  166. )
  167. def test_with_exclude(self):
  168. self.assertSequenceEqual(
  169. Author.objects.annotate(
  170. book_alice=FilteredRelation(
  171. "book", condition=Q(book__title__iexact="poem by alice")
  172. ),
  173. ).exclude(book_alice__isnull=False),
  174. [self.author2],
  175. )
  176. def test_with_join_and_complex_condition(self):
  177. self.assertSequenceEqual(
  178. Author.objects.annotate(
  179. book_alice=FilteredRelation(
  180. "book",
  181. condition=Q(
  182. Q(book__title__iexact="poem by alice")
  183. | Q(book__state=Book.RENTED)
  184. ),
  185. ),
  186. ).filter(book_alice__isnull=False),
  187. [self.author1],
  188. )
  189. def test_internal_queryset_alias_mapping(self):
  190. queryset = Author.objects.annotate(
  191. book_alice=FilteredRelation(
  192. "book", condition=Q(book__title__iexact="poem by alice")
  193. ),
  194. ).filter(book_alice__isnull=False)
  195. self.assertIn(
  196. "INNER JOIN {} book_alice ON".format(
  197. connection.ops.quote_name("filtered_relation_book")
  198. ),
  199. str(queryset.query),
  200. )
  201. def test_multiple(self):
  202. qs = (
  203. Author.objects.annotate(
  204. book_title_alice=FilteredRelation(
  205. "book", condition=Q(book__title__contains="Alice")
  206. ),
  207. book_title_jane=FilteredRelation(
  208. "book", condition=Q(book__title__icontains="Jane")
  209. ),
  210. )
  211. .filter(name="Jane")
  212. .values("book_title_alice__title", "book_title_jane__title")
  213. )
  214. empty = "" if connection.features.interprets_empty_strings_as_nulls else None
  215. self.assertCountEqual(
  216. qs,
  217. [
  218. {
  219. "book_title_alice__title": empty,
  220. "book_title_jane__title": "The book by Jane A",
  221. },
  222. {
  223. "book_title_alice__title": empty,
  224. "book_title_jane__title": "The book by Jane B",
  225. },
  226. ],
  227. )
  228. def test_with_multiple_filter(self):
  229. self.assertSequenceEqual(
  230. Author.objects.annotate(
  231. book_editor_a=FilteredRelation(
  232. "book",
  233. condition=Q(
  234. book__title__icontains="book", book__editor_id=self.editor_a.pk
  235. ),
  236. ),
  237. ).filter(book_editor_a__isnull=False),
  238. [self.author1],
  239. )
  240. def test_multiple_times(self):
  241. self.assertSequenceEqual(
  242. Author.objects.annotate(
  243. book_title_alice=FilteredRelation(
  244. "book", condition=Q(book__title__icontains="alice")
  245. ),
  246. )
  247. .filter(book_title_alice__isnull=False)
  248. .filter(book_title_alice__isnull=False)
  249. .distinct(),
  250. [self.author1],
  251. )
  252. def test_exclude_relation_with_join(self):
  253. self.assertSequenceEqual(
  254. Author.objects.annotate(
  255. book_alice=FilteredRelation(
  256. "book", condition=~Q(book__title__icontains="alice")
  257. ),
  258. )
  259. .filter(book_alice__isnull=False)
  260. .distinct(),
  261. [self.author2],
  262. )
  263. def test_with_m2m(self):
  264. qs = Author.objects.annotate(
  265. favorite_books_written_by_jane=FilteredRelation(
  266. "favorite_books",
  267. condition=Q(favorite_books__in=[self.book2]),
  268. ),
  269. ).filter(favorite_books_written_by_jane__isnull=False)
  270. self.assertSequenceEqual(qs, [self.author1])
  271. def test_with_m2m_deep(self):
  272. qs = Author.objects.annotate(
  273. favorite_books_written_by_jane=FilteredRelation(
  274. "favorite_books",
  275. condition=Q(favorite_books__author=self.author2),
  276. ),
  277. ).filter(favorite_books_written_by_jane__title="The book by Jane B")
  278. self.assertSequenceEqual(qs, [self.author1])
  279. def test_with_m2m_multijoin(self):
  280. qs = (
  281. Author.objects.annotate(
  282. favorite_books_written_by_jane=FilteredRelation(
  283. "favorite_books",
  284. condition=Q(favorite_books__author=self.author2),
  285. )
  286. )
  287. .filter(favorite_books_written_by_jane__editor__name="b")
  288. .distinct()
  289. )
  290. self.assertSequenceEqual(qs, [self.author1])
  291. def test_values_list(self):
  292. self.assertSequenceEqual(
  293. Author.objects.annotate(
  294. book_alice=FilteredRelation(
  295. "book", condition=Q(book__title__iexact="poem by alice")
  296. ),
  297. )
  298. .filter(book_alice__isnull=False)
  299. .values_list("book_alice__title", flat=True),
  300. ["Poem by Alice"],
  301. )
  302. def test_values(self):
  303. self.assertSequenceEqual(
  304. Author.objects.annotate(
  305. book_alice=FilteredRelation(
  306. "book", condition=Q(book__title__iexact="poem by alice")
  307. ),
  308. )
  309. .filter(book_alice__isnull=False)
  310. .values(),
  311. [
  312. {
  313. "id": self.author1.pk,
  314. "name": "Alice",
  315. "content_type_id": None,
  316. "object_id": None,
  317. }
  318. ],
  319. )
  320. def test_extra(self):
  321. self.assertSequenceEqual(
  322. Author.objects.annotate(
  323. book_alice=FilteredRelation(
  324. "book", condition=Q(book__title__iexact="poem by alice")
  325. ),
  326. )
  327. .filter(book_alice__isnull=False)
  328. .extra(where=["1 = 1"]),
  329. [self.author1],
  330. )
  331. @skipUnlessDBFeature("supports_select_union")
  332. def test_union(self):
  333. qs1 = Author.objects.annotate(
  334. book_alice=FilteredRelation(
  335. "book", condition=Q(book__title__iexact="poem by alice")
  336. ),
  337. ).filter(book_alice__isnull=False)
  338. qs2 = Author.objects.annotate(
  339. book_jane=FilteredRelation(
  340. "book", condition=Q(book__title__iexact="the book by jane a")
  341. ),
  342. ).filter(book_jane__isnull=False)
  343. self.assertSequenceEqual(qs1.union(qs2), [self.author1, self.author2])
  344. @skipUnlessDBFeature("supports_select_intersection")
  345. def test_intersection(self):
  346. qs1 = Author.objects.annotate(
  347. book_alice=FilteredRelation(
  348. "book", condition=Q(book__title__iexact="poem by alice")
  349. ),
  350. ).filter(book_alice__isnull=False)
  351. qs2 = Author.objects.annotate(
  352. book_jane=FilteredRelation(
  353. "book", condition=Q(book__title__iexact="the book by jane a")
  354. ),
  355. ).filter(book_jane__isnull=False)
  356. self.assertSequenceEqual(qs1.intersection(qs2), [])
  357. @skipUnlessDBFeature("supports_select_difference")
  358. def test_difference(self):
  359. qs1 = Author.objects.annotate(
  360. book_alice=FilteredRelation(
  361. "book", condition=Q(book__title__iexact="poem by alice")
  362. ),
  363. ).filter(book_alice__isnull=False)
  364. qs2 = Author.objects.annotate(
  365. book_jane=FilteredRelation(
  366. "book", condition=Q(book__title__iexact="the book by jane a")
  367. ),
  368. ).filter(book_jane__isnull=False)
  369. self.assertSequenceEqual(qs1.difference(qs2), [self.author1])
  370. def test_select_for_update(self):
  371. self.assertSequenceEqual(
  372. Author.objects.annotate(
  373. book_jane=FilteredRelation(
  374. "book", condition=Q(book__title__iexact="the book by jane a")
  375. ),
  376. )
  377. .filter(book_jane__isnull=False)
  378. .select_for_update(),
  379. [self.author2],
  380. )
  381. def test_defer(self):
  382. # One query for the list and one query for the deferred title.
  383. with self.assertNumQueries(2):
  384. self.assertQuerySetEqual(
  385. Author.objects.annotate(
  386. book_alice=FilteredRelation(
  387. "book", condition=Q(book__title__iexact="poem by alice")
  388. ),
  389. )
  390. .filter(book_alice__isnull=False)
  391. .select_related("book_alice")
  392. .defer("book_alice__title"),
  393. ["Poem by Alice"],
  394. lambda author: author.book_alice.title,
  395. )
  396. def test_only_not_supported(self):
  397. msg = "only() is not supported with FilteredRelation."
  398. with self.assertRaisesMessage(ValueError, msg):
  399. Author.objects.annotate(
  400. book_alice=FilteredRelation(
  401. "book", condition=Q(book__title__iexact="poem by alice")
  402. ),
  403. ).filter(book_alice__isnull=False).select_related("book_alice").only(
  404. "book_alice__state"
  405. )
  406. def test_as_subquery(self):
  407. inner_qs = Author.objects.annotate(
  408. book_alice=FilteredRelation(
  409. "book", condition=Q(book__title__iexact="poem by alice")
  410. ),
  411. ).filter(book_alice__isnull=False)
  412. qs = Author.objects.filter(id__in=inner_qs)
  413. self.assertSequenceEqual(qs, [self.author1])
  414. def test_nested_foreign_key(self):
  415. qs = (
  416. Author.objects.annotate(
  417. book_editor_worked_with=FilteredRelation(
  418. "book__editor",
  419. condition=Q(book__title__icontains="book by"),
  420. ),
  421. )
  422. .filter(
  423. book_editor_worked_with__isnull=False,
  424. )
  425. .select_related(
  426. "book_editor_worked_with",
  427. )
  428. .order_by("pk", "book_editor_worked_with__pk")
  429. )
  430. with self.assertNumQueries(1):
  431. self.assertQuerySetEqual(
  432. qs,
  433. [
  434. (self.author1, self.editor_a),
  435. (self.author2, self.editor_b),
  436. (self.author2, self.editor_b),
  437. ],
  438. lambda x: (x, x.book_editor_worked_with),
  439. )
  440. def test_nested_foreign_key_nested_field(self):
  441. qs = (
  442. Author.objects.annotate(
  443. book_editor_worked_with=FilteredRelation(
  444. "book__editor", condition=Q(book__title__icontains="book by")
  445. ),
  446. )
  447. .filter(
  448. book_editor_worked_with__isnull=False,
  449. )
  450. .values(
  451. "name",
  452. "book_editor_worked_with__name",
  453. )
  454. .order_by("name", "book_editor_worked_with__name")
  455. .distinct()
  456. )
  457. self.assertSequenceEqual(
  458. qs,
  459. [
  460. {
  461. "name": self.author1.name,
  462. "book_editor_worked_with__name": self.editor_a.name,
  463. },
  464. {
  465. "name": self.author2.name,
  466. "book_editor_worked_with__name": self.editor_b.name,
  467. },
  468. ],
  469. )
  470. def test_nested_foreign_key_filtered_base_object(self):
  471. qs = (
  472. Author.objects.annotate(
  473. alice_editors=FilteredRelation(
  474. "book__editor",
  475. condition=Q(name="Alice"),
  476. ),
  477. )
  478. .values(
  479. "name",
  480. "alice_editors__pk",
  481. )
  482. .order_by("name", "alice_editors__name")
  483. .distinct()
  484. )
  485. self.assertSequenceEqual(
  486. qs,
  487. [
  488. {"name": self.author1.name, "alice_editors__pk": self.editor_a.pk},
  489. {"name": self.author2.name, "alice_editors__pk": None},
  490. ],
  491. )
  492. def test_nested_m2m_filtered(self):
  493. qs = (
  494. Book.objects.annotate(
  495. favorite_book=FilteredRelation(
  496. "author__favorite_books",
  497. condition=Q(author__favorite_books__title__icontains="book by"),
  498. ),
  499. )
  500. .values(
  501. "title",
  502. "favorite_book__pk",
  503. )
  504. .order_by("title", "favorite_book__title")
  505. )
  506. self.assertSequenceEqual(
  507. qs,
  508. [
  509. {"title": self.book1.title, "favorite_book__pk": self.book2.pk},
  510. {"title": self.book1.title, "favorite_book__pk": self.book3.pk},
  511. {"title": self.book4.title, "favorite_book__pk": self.book2.pk},
  512. {"title": self.book4.title, "favorite_book__pk": self.book3.pk},
  513. {"title": self.book2.title, "favorite_book__pk": None},
  514. {"title": self.book3.title, "favorite_book__pk": None},
  515. ],
  516. )
  517. def test_nested_chained_relations(self):
  518. qs = (
  519. Author.objects.annotate(
  520. my_books=FilteredRelation(
  521. "book",
  522. condition=Q(book__title__icontains="book by"),
  523. ),
  524. preferred_by_authors=FilteredRelation(
  525. "my_books__preferred_by_authors",
  526. condition=Q(my_books__preferred_by_authors__name="Alice"),
  527. ),
  528. )
  529. .annotate(
  530. author=F("name"),
  531. book_title=F("my_books__title"),
  532. preferred_by_author_pk=F("preferred_by_authors"),
  533. )
  534. .order_by("author", "book_title", "preferred_by_author_pk")
  535. )
  536. self.assertQuerySetEqual(
  537. qs,
  538. [
  539. ("Alice", "The book by Alice", None),
  540. ("Jane", "The book by Jane A", self.author1.pk),
  541. ("Jane", "The book by Jane B", self.author1.pk),
  542. ],
  543. lambda x: (x.author, x.book_title, x.preferred_by_author_pk),
  544. )
  545. def test_deep_nested_foreign_key(self):
  546. qs = (
  547. Book.objects.annotate(
  548. author_favorite_book_editor=FilteredRelation(
  549. "author__favorite_books__editor",
  550. condition=Q(author__favorite_books__title__icontains="Jane A"),
  551. ),
  552. )
  553. .filter(
  554. author_favorite_book_editor__isnull=False,
  555. )
  556. .select_related(
  557. "author_favorite_book_editor",
  558. )
  559. .order_by("pk", "author_favorite_book_editor__pk")
  560. )
  561. with self.assertNumQueries(1):
  562. self.assertQuerySetEqual(
  563. qs,
  564. [
  565. (self.book1, self.editor_b),
  566. (self.book4, self.editor_b),
  567. ],
  568. lambda x: (x, x.author_favorite_book_editor),
  569. )
  570. def test_relation_name_lookup(self):
  571. msg = (
  572. "FilteredRelation's relation_name cannot contain lookups (got "
  573. "'book__title__icontains')."
  574. )
  575. with self.assertRaisesMessage(ValueError, msg):
  576. Author.objects.annotate(
  577. book_title=FilteredRelation(
  578. "book__title__icontains",
  579. condition=Q(book__title="Poem by Alice"),
  580. ),
  581. )
  582. def test_condition_outside_relation_name(self):
  583. msg = (
  584. "FilteredRelation's condition doesn't support relations outside "
  585. "the 'book__editor' (got 'book__author__name__icontains')."
  586. )
  587. with self.assertRaisesMessage(ValueError, msg):
  588. Author.objects.annotate(
  589. book_editor=FilteredRelation(
  590. "book__editor",
  591. condition=Q(book__author__name__icontains="book"),
  592. ),
  593. )
  594. def test_condition_with_exact_lookup_outside_relation_name(self):
  595. qs = Author.objects.annotate(
  596. book_editor=FilteredRelation(
  597. "book__editor",
  598. condition=Q(book__author__name="book"),
  599. ),
  600. ).filter(book_editor__isnull=True)
  601. self.assertEqual(qs.count(), 4)
  602. def test_condition_with_func_and_lookup_outside_relation_name(self):
  603. qs = Author.objects.annotate(
  604. book_editor=FilteredRelation(
  605. "book__editor",
  606. condition=Q(
  607. book__title=Concat(Value("The book by "), F("book__author__name"))
  608. ),
  609. ),
  610. ).filter(book_editor__isnull=False)
  611. self.assertEquals(qs.count(), 1)
  612. def test_condition_deeper_relation_name(self):
  613. msg = (
  614. "FilteredRelation's condition doesn't support nested relations "
  615. "deeper than the relation_name (got "
  616. "'book__editor__name__icontains' for 'book')."
  617. )
  618. with self.assertRaisesMessage(ValueError, msg):
  619. Author.objects.annotate(
  620. book_editor=FilteredRelation(
  621. "book",
  622. condition=Q(book__editor__name__icontains="b"),
  623. ),
  624. )
  625. def test_with_empty_relation_name_error(self):
  626. with self.assertRaisesMessage(ValueError, "relation_name cannot be empty."):
  627. FilteredRelation("", condition=Q(blank=""))
  628. def test_with_condition_as_expression_error(self):
  629. msg = "condition argument must be a Q() instance."
  630. expression = Case(
  631. When(book__title__iexact="poem by alice", then=True),
  632. default=False,
  633. )
  634. with self.assertRaisesMessage(ValueError, msg):
  635. FilteredRelation("book", condition=expression)
  636. def test_with_prefetch_related(self):
  637. msg = "prefetch_related() is not supported with FilteredRelation."
  638. qs = Author.objects.annotate(
  639. book_title_contains_b=FilteredRelation(
  640. "book", condition=Q(book__title__icontains="b")
  641. ),
  642. ).filter(
  643. book_title_contains_b__isnull=False,
  644. )
  645. with self.assertRaisesMessage(ValueError, msg):
  646. qs.prefetch_related("book_title_contains_b")
  647. with self.assertRaisesMessage(ValueError, msg):
  648. qs.prefetch_related("book_title_contains_b__editor")
  649. def test_with_generic_foreign_key(self):
  650. self.assertSequenceEqual(
  651. Book.objects.annotate(
  652. generic_authored_book=FilteredRelation(
  653. "generic_author", condition=Q(generic_author__isnull=False)
  654. ),
  655. ).filter(generic_authored_book__isnull=False),
  656. [self.book1],
  657. )
  658. def test_eq(self):
  659. self.assertEqual(
  660. FilteredRelation("book", condition=Q(book__title="b")), mock.ANY
  661. )
  662. class FilteredRelationAggregationTests(TestCase):
  663. @classmethod
  664. def setUpTestData(cls):
  665. cls.author1 = Author.objects.create(name="Alice")
  666. cls.editor_a = Editor.objects.create(name="a")
  667. cls.book1 = Book.objects.create(
  668. title="Poem by Alice",
  669. editor=cls.editor_a,
  670. author=cls.author1,
  671. )
  672. cls.borrower1 = Borrower.objects.create(name="Jenny")
  673. cls.borrower2 = Borrower.objects.create(name="Kevin")
  674. # borrower 1 reserves, rents, and returns book1.
  675. Reservation.objects.create(
  676. borrower=cls.borrower1,
  677. book=cls.book1,
  678. state=Reservation.STOPPED,
  679. )
  680. RentalSession.objects.create(
  681. borrower=cls.borrower1,
  682. book=cls.book1,
  683. state=RentalSession.STOPPED,
  684. )
  685. # borrower2 reserves, rents, and returns book1.
  686. Reservation.objects.create(
  687. borrower=cls.borrower2,
  688. book=cls.book1,
  689. state=Reservation.STOPPED,
  690. )
  691. RentalSession.objects.create(
  692. borrower=cls.borrower2,
  693. book=cls.book1,
  694. state=RentalSession.STOPPED,
  695. )
  696. def test_aggregate(self):
  697. """
  698. filtered_relation() not only improves performance but also creates
  699. correct results when aggregating with multiple LEFT JOINs.
  700. Books can be reserved then rented by a borrower. Each reservation and
  701. rental session are recorded with Reservation and RentalSession models.
  702. Every time a reservation or a rental session is over, their state is
  703. changed to 'stopped'.
  704. Goal: Count number of books that are either currently reserved or
  705. rented by borrower1 or available.
  706. """
  707. qs = (
  708. Book.objects.annotate(
  709. is_reserved_or_rented_by=Case(
  710. When(
  711. reservation__state=Reservation.NEW,
  712. then=F("reservation__borrower__pk"),
  713. ),
  714. When(
  715. rental_session__state=RentalSession.NEW,
  716. then=F("rental_session__borrower__pk"),
  717. ),
  718. default=None,
  719. )
  720. )
  721. .filter(
  722. Q(is_reserved_or_rented_by=self.borrower1.pk) | Q(state=Book.AVAILABLE)
  723. )
  724. .distinct()
  725. )
  726. self.assertEqual(qs.count(), 1)
  727. # If count is equal to 1, the same aggregation should return in the
  728. # same result but it returns 4.
  729. self.assertSequenceEqual(
  730. qs.annotate(total=Count("pk")).values("total"), [{"total": 4}]
  731. )
  732. # With FilteredRelation, the result is as expected (1).
  733. qs = (
  734. Book.objects.annotate(
  735. active_reservations=FilteredRelation(
  736. "reservation",
  737. condition=Q(
  738. reservation__state=Reservation.NEW,
  739. reservation__borrower=self.borrower1,
  740. ),
  741. ),
  742. )
  743. .annotate(
  744. active_rental_sessions=FilteredRelation(
  745. "rental_session",
  746. condition=Q(
  747. rental_session__state=RentalSession.NEW,
  748. rental_session__borrower=self.borrower1,
  749. ),
  750. ),
  751. )
  752. .filter(
  753. (
  754. Q(active_reservations__isnull=False)
  755. | Q(active_rental_sessions__isnull=False)
  756. )
  757. | Q(state=Book.AVAILABLE)
  758. )
  759. .distinct()
  760. )
  761. self.assertEqual(qs.count(), 1)
  762. self.assertSequenceEqual(
  763. qs.annotate(total=Count("pk")).values("total"), [{"total": 1}]
  764. )
  765. def test_condition_spans_join(self):
  766. self.assertSequenceEqual(
  767. Book.objects.annotate(
  768. contains_editor_author=FilteredRelation(
  769. "author", condition=Q(author__name__icontains=F("editor__name"))
  770. )
  771. ).filter(
  772. contains_editor_author__isnull=False,
  773. ),
  774. [self.book1],
  775. )
  776. def test_condition_spans_join_chained(self):
  777. self.assertSequenceEqual(
  778. Book.objects.annotate(
  779. contains_editor_author=FilteredRelation(
  780. "author", condition=Q(author__name__icontains=F("editor__name"))
  781. ),
  782. contains_editor_author_ref=FilteredRelation(
  783. "author",
  784. condition=Q(author__name=F("contains_editor_author__name")),
  785. ),
  786. ).filter(
  787. contains_editor_author_ref__isnull=False,
  788. ),
  789. [self.book1],
  790. )
  791. def test_condition_self_ref(self):
  792. self.assertSequenceEqual(
  793. Book.objects.annotate(
  794. contains_author=FilteredRelation(
  795. "author",
  796. condition=Q(title__icontains=F("author__name")),
  797. )
  798. ).filter(
  799. contains_author__isnull=False,
  800. ),
  801. [self.book1],
  802. )
  803. class FilteredRelationAnalyticalAggregationTests(TestCase):
  804. @classmethod
  805. def setUpTestData(cls):
  806. author = Author.objects.create(name="Author")
  807. editor = Editor.objects.create(name="Editor")
  808. cls.book1 = Book.objects.create(
  809. title="Poem by Alice",
  810. editor=editor,
  811. author=author,
  812. )
  813. cls.book2 = Book.objects.create(
  814. title="The book by Jane A",
  815. editor=editor,
  816. author=author,
  817. )
  818. cls.book3 = Book.objects.create(
  819. title="The book by Jane B",
  820. editor=editor,
  821. author=author,
  822. )
  823. cls.seller1 = Seller.objects.create(name="Seller 1")
  824. cls.seller2 = Seller.objects.create(name="Seller 2")
  825. cls.usd = Currency.objects.create(currency="USD")
  826. cls.eur = Currency.objects.create(currency="EUR")
  827. cls.sales_date1 = date(2020, 7, 6)
  828. cls.sales_date2 = date(2020, 7, 7)
  829. ExchangeRate.objects.bulk_create(
  830. [
  831. ExchangeRate(
  832. rate_date=cls.sales_date1,
  833. from_currency=cls.usd,
  834. to_currency=cls.eur,
  835. rate=0.40,
  836. ),
  837. ExchangeRate(
  838. rate_date=cls.sales_date1,
  839. from_currency=cls.eur,
  840. to_currency=cls.usd,
  841. rate=1.60,
  842. ),
  843. ExchangeRate(
  844. rate_date=cls.sales_date2,
  845. from_currency=cls.usd,
  846. to_currency=cls.eur,
  847. rate=0.50,
  848. ),
  849. ExchangeRate(
  850. rate_date=cls.sales_date2,
  851. from_currency=cls.eur,
  852. to_currency=cls.usd,
  853. rate=1.50,
  854. ),
  855. ExchangeRate(
  856. rate_date=cls.sales_date2,
  857. from_currency=cls.usd,
  858. to_currency=cls.usd,
  859. rate=1.00,
  860. ),
  861. ]
  862. )
  863. BookDailySales.objects.bulk_create(
  864. [
  865. BookDailySales(
  866. book=cls.book1,
  867. sale_date=cls.sales_date1,
  868. currency=cls.usd,
  869. sales=100.00,
  870. seller=cls.seller1,
  871. ),
  872. BookDailySales(
  873. book=cls.book2,
  874. sale_date=cls.sales_date1,
  875. currency=cls.eur,
  876. sales=200.00,
  877. seller=cls.seller1,
  878. ),
  879. BookDailySales(
  880. book=cls.book1,
  881. sale_date=cls.sales_date2,
  882. currency=cls.usd,
  883. sales=50.00,
  884. seller=cls.seller2,
  885. ),
  886. BookDailySales(
  887. book=cls.book2,
  888. sale_date=cls.sales_date2,
  889. currency=cls.eur,
  890. sales=100.00,
  891. seller=cls.seller2,
  892. ),
  893. ]
  894. )
  895. def test_aggregate(self):
  896. tests = [
  897. Q(daily_sales__sale_date__gte=self.sales_date2),
  898. ~Q(daily_sales__seller=self.seller1),
  899. ]
  900. for condition in tests:
  901. with self.subTest(condition=condition):
  902. qs = (
  903. Book.objects.annotate(
  904. recent_sales=FilteredRelation(
  905. "daily_sales", condition=condition
  906. ),
  907. recent_sales_rates=FilteredRelation(
  908. "recent_sales__currency__rates_from",
  909. condition=Q(
  910. recent_sales__currency__rates_from__rate_date=F(
  911. "recent_sales__sale_date"
  912. ),
  913. recent_sales__currency__rates_from__to_currency=(
  914. self.usd
  915. ),
  916. ),
  917. ),
  918. )
  919. .annotate(
  920. sales_sum=Sum(
  921. F("recent_sales__sales") * F("recent_sales_rates__rate"),
  922. output_field=DecimalField(),
  923. ),
  924. )
  925. .values("title", "sales_sum")
  926. .order_by(
  927. F("sales_sum").desc(nulls_last=True),
  928. )
  929. )
  930. self.assertSequenceEqual(
  931. qs,
  932. [
  933. {"title": self.book2.title, "sales_sum": Decimal(150.00)},
  934. {"title": self.book1.title, "sales_sum": Decimal(50.00)},
  935. {"title": self.book3.title, "sales_sum": None},
  936. ],
  937. )