tests.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from math import ceil
  2. from operator import attrgetter
  3. from django.db import IntegrityError, NotSupportedError, connection
  4. from django.db.models import FileField, Value
  5. from django.db.models.functions import Lower
  6. from django.test import (
  7. TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature,
  8. )
  9. from .models import (
  10. Country, NoFields, NullableFields, Pizzeria, ProxyCountry,
  11. ProxyMultiCountry, ProxyMultiProxyCountry, ProxyProxyCountry, Restaurant,
  12. State, TwoFields,
  13. )
  14. class BulkCreateTests(TestCase):
  15. def setUp(self):
  16. self.data = [
  17. Country(name="United States of America", iso_two_letter="US"),
  18. Country(name="The Netherlands", iso_two_letter="NL"),
  19. Country(name="Germany", iso_two_letter="DE"),
  20. Country(name="Czech Republic", iso_two_letter="CZ")
  21. ]
  22. def test_simple(self):
  23. created = Country.objects.bulk_create(self.data)
  24. self.assertEqual(created, self.data)
  25. self.assertQuerysetEqual(Country.objects.order_by("-name"), [
  26. "United States of America", "The Netherlands", "Germany", "Czech Republic"
  27. ], attrgetter("name"))
  28. created = Country.objects.bulk_create([])
  29. self.assertEqual(created, [])
  30. self.assertEqual(Country.objects.count(), 4)
  31. @skipUnlessDBFeature('has_bulk_insert')
  32. def test_efficiency(self):
  33. with self.assertNumQueries(1):
  34. Country.objects.bulk_create(self.data)
  35. @skipUnlessDBFeature('has_bulk_insert')
  36. def test_long_non_ascii_text(self):
  37. """
  38. Inserting non-ASCII values with a length in the range 2001 to 4000
  39. characters, i.e. 4002 to 8000 bytes, must be set as a CLOB on Oracle
  40. (#22144).
  41. """
  42. Country.objects.bulk_create([Country(description='Ж' * 3000)])
  43. self.assertEqual(Country.objects.count(), 1)
  44. @skipUnlessDBFeature('has_bulk_insert')
  45. def test_long_and_short_text(self):
  46. Country.objects.bulk_create([
  47. Country(description='a' * 4001),
  48. Country(description='a'),
  49. Country(description='Ж' * 2001),
  50. Country(description='Ж'),
  51. ])
  52. self.assertEqual(Country.objects.count(), 4)
  53. def test_multi_table_inheritance_unsupported(self):
  54. expected_message = "Can't bulk create a multi-table inherited model"
  55. with self.assertRaisesMessage(ValueError, expected_message):
  56. Pizzeria.objects.bulk_create([
  57. Pizzeria(name="The Art of Pizza"),
  58. ])
  59. with self.assertRaisesMessage(ValueError, expected_message):
  60. ProxyMultiCountry.objects.bulk_create([
  61. ProxyMultiCountry(name="Fillory", iso_two_letter="FL"),
  62. ])
  63. with self.assertRaisesMessage(ValueError, expected_message):
  64. ProxyMultiProxyCountry.objects.bulk_create([
  65. ProxyMultiProxyCountry(name="Fillory", iso_two_letter="FL"),
  66. ])
  67. def test_proxy_inheritance_supported(self):
  68. ProxyCountry.objects.bulk_create([
  69. ProxyCountry(name="Qwghlm", iso_two_letter="QW"),
  70. Country(name="Tortall", iso_two_letter="TA"),
  71. ])
  72. self.assertQuerysetEqual(ProxyCountry.objects.all(), {
  73. "Qwghlm", "Tortall"
  74. }, attrgetter("name"), ordered=False)
  75. ProxyProxyCountry.objects.bulk_create([
  76. ProxyProxyCountry(name="Netherlands", iso_two_letter="NT"),
  77. ])
  78. self.assertQuerysetEqual(ProxyProxyCountry.objects.all(), {
  79. "Qwghlm", "Tortall", "Netherlands",
  80. }, attrgetter("name"), ordered=False)
  81. def test_non_auto_increment_pk(self):
  82. State.objects.bulk_create([
  83. State(two_letter_code=s)
  84. for s in ["IL", "NY", "CA", "ME"]
  85. ])
  86. self.assertQuerysetEqual(State.objects.order_by("two_letter_code"), [
  87. "CA", "IL", "ME", "NY",
  88. ], attrgetter("two_letter_code"))
  89. @skipUnlessDBFeature('has_bulk_insert')
  90. def test_non_auto_increment_pk_efficiency(self):
  91. with self.assertNumQueries(1):
  92. State.objects.bulk_create([
  93. State(two_letter_code=s)
  94. for s in ["IL", "NY", "CA", "ME"]
  95. ])
  96. self.assertQuerysetEqual(State.objects.order_by("two_letter_code"), [
  97. "CA", "IL", "ME", "NY",
  98. ], attrgetter("two_letter_code"))
  99. @skipIfDBFeature('allows_auto_pk_0')
  100. def test_zero_as_autoval(self):
  101. """
  102. Zero as id for AutoField should raise exception in MySQL, because MySQL
  103. does not allow zero for automatic primary key.
  104. """
  105. valid_country = Country(name='Germany', iso_two_letter='DE')
  106. invalid_country = Country(id=0, name='Poland', iso_two_letter='PL')
  107. msg = 'The database backend does not accept 0 as a value for AutoField.'
  108. with self.assertRaisesMessage(ValueError, msg):
  109. Country.objects.bulk_create([valid_country, invalid_country])
  110. def test_batch_same_vals(self):
  111. # SQLite had a problem where all the same-valued models were
  112. # collapsed to one insert.
  113. Restaurant.objects.bulk_create([
  114. Restaurant(name='foo') for i in range(0, 2)
  115. ])
  116. self.assertEqual(Restaurant.objects.count(), 2)
  117. def test_large_batch(self):
  118. TwoFields.objects.bulk_create([
  119. TwoFields(f1=i, f2=i + 1) for i in range(0, 1001)
  120. ])
  121. self.assertEqual(TwoFields.objects.count(), 1001)
  122. self.assertEqual(
  123. TwoFields.objects.filter(f1__gte=450, f1__lte=550).count(),
  124. 101)
  125. self.assertEqual(TwoFields.objects.filter(f2__gte=901).count(), 101)
  126. @skipUnlessDBFeature('has_bulk_insert')
  127. def test_large_single_field_batch(self):
  128. # SQLite had a problem with more than 500 UNIONed selects in single
  129. # query.
  130. Restaurant.objects.bulk_create([
  131. Restaurant() for i in range(0, 501)
  132. ])
  133. @skipUnlessDBFeature('has_bulk_insert')
  134. def test_large_batch_efficiency(self):
  135. with override_settings(DEBUG=True):
  136. connection.queries_log.clear()
  137. TwoFields.objects.bulk_create([
  138. TwoFields(f1=i, f2=i + 1) for i in range(0, 1001)
  139. ])
  140. self.assertLess(len(connection.queries), 10)
  141. def test_large_batch_mixed(self):
  142. """
  143. Test inserting a large batch with objects having primary key set
  144. mixed together with objects without PK set.
  145. """
  146. TwoFields.objects.bulk_create([
  147. TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i + 1)
  148. for i in range(100000, 101000)
  149. ])
  150. self.assertEqual(TwoFields.objects.count(), 1000)
  151. # We can't assume much about the ID's created, except that the above
  152. # created IDs must exist.
  153. id_range = range(100000, 101000, 2)
  154. self.assertEqual(TwoFields.objects.filter(id__in=id_range).count(), 500)
  155. self.assertEqual(TwoFields.objects.exclude(id__in=id_range).count(), 500)
  156. @skipUnlessDBFeature('has_bulk_insert')
  157. def test_large_batch_mixed_efficiency(self):
  158. """
  159. Test inserting a large batch with objects having primary key set
  160. mixed together with objects without PK set.
  161. """
  162. with override_settings(DEBUG=True):
  163. connection.queries_log.clear()
  164. TwoFields.objects.bulk_create([
  165. TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i + 1)
  166. for i in range(100000, 101000)])
  167. self.assertLess(len(connection.queries), 10)
  168. def test_explicit_batch_size(self):
  169. objs = [TwoFields(f1=i, f2=i) for i in range(0, 4)]
  170. num_objs = len(objs)
  171. TwoFields.objects.bulk_create(objs, batch_size=1)
  172. self.assertEqual(TwoFields.objects.count(), num_objs)
  173. TwoFields.objects.all().delete()
  174. TwoFields.objects.bulk_create(objs, batch_size=2)
  175. self.assertEqual(TwoFields.objects.count(), num_objs)
  176. TwoFields.objects.all().delete()
  177. TwoFields.objects.bulk_create(objs, batch_size=3)
  178. self.assertEqual(TwoFields.objects.count(), num_objs)
  179. TwoFields.objects.all().delete()
  180. TwoFields.objects.bulk_create(objs, batch_size=num_objs)
  181. self.assertEqual(TwoFields.objects.count(), num_objs)
  182. def test_empty_model(self):
  183. NoFields.objects.bulk_create([NoFields() for i in range(2)])
  184. self.assertEqual(NoFields.objects.count(), 2)
  185. @skipUnlessDBFeature('has_bulk_insert')
  186. def test_explicit_batch_size_efficiency(self):
  187. objs = [TwoFields(f1=i, f2=i) for i in range(0, 100)]
  188. with self.assertNumQueries(2):
  189. TwoFields.objects.bulk_create(objs, 50)
  190. TwoFields.objects.all().delete()
  191. with self.assertNumQueries(1):
  192. TwoFields.objects.bulk_create(objs, len(objs))
  193. @skipUnlessDBFeature('has_bulk_insert')
  194. def test_explicit_batch_size_respects_max_batch_size(self):
  195. objs = [Country() for i in range(1000)]
  196. fields = ['name', 'iso_two_letter', 'description']
  197. max_batch_size = max(connection.ops.bulk_batch_size(fields, objs), 1)
  198. with self.assertNumQueries(ceil(len(objs) / max_batch_size)):
  199. Country.objects.bulk_create(objs, batch_size=max_batch_size + 1)
  200. @skipUnlessDBFeature('has_bulk_insert')
  201. def test_bulk_insert_expressions(self):
  202. Restaurant.objects.bulk_create([
  203. Restaurant(name="Sam's Shake Shack"),
  204. Restaurant(name=Lower(Value("Betty's Beetroot Bar")))
  205. ])
  206. bbb = Restaurant.objects.filter(name="betty's beetroot bar")
  207. self.assertEqual(bbb.count(), 1)
  208. @skipUnlessDBFeature('has_bulk_insert')
  209. def test_bulk_insert_nullable_fields(self):
  210. # NULL can be mixed with other values in nullable fields
  211. nullable_fields = [field for field in NullableFields._meta.get_fields() if field.name != 'id']
  212. NullableFields.objects.bulk_create([
  213. NullableFields(**{field.name: None}) for field in nullable_fields
  214. ])
  215. self.assertEqual(NullableFields.objects.count(), len(nullable_fields))
  216. for field in nullable_fields:
  217. with self.subTest(field=field):
  218. field_value = '' if isinstance(field, FileField) else None
  219. self.assertEqual(NullableFields.objects.filter(**{field.name: field_value}).count(), 1)
  220. @skipUnlessDBFeature('can_return_rows_from_bulk_insert')
  221. def test_set_pk_and_insert_single_item(self):
  222. with self.assertNumQueries(1):
  223. countries = Country.objects.bulk_create([self.data[0]])
  224. self.assertEqual(len(countries), 1)
  225. self.assertEqual(Country.objects.get(pk=countries[0].pk), countries[0])
  226. @skipUnlessDBFeature('can_return_rows_from_bulk_insert')
  227. def test_set_pk_and_query_efficiency(self):
  228. with self.assertNumQueries(1):
  229. countries = Country.objects.bulk_create(self.data)
  230. self.assertEqual(len(countries), 4)
  231. self.assertEqual(Country.objects.get(pk=countries[0].pk), countries[0])
  232. self.assertEqual(Country.objects.get(pk=countries[1].pk), countries[1])
  233. self.assertEqual(Country.objects.get(pk=countries[2].pk), countries[2])
  234. self.assertEqual(Country.objects.get(pk=countries[3].pk), countries[3])
  235. @skipUnlessDBFeature('can_return_rows_from_bulk_insert')
  236. def test_set_state(self):
  237. country_nl = Country(name='Netherlands', iso_two_letter='NL')
  238. country_be = Country(name='Belgium', iso_two_letter='BE')
  239. Country.objects.bulk_create([country_nl])
  240. country_be.save()
  241. # Objects save via bulk_create() and save() should have equal state.
  242. self.assertEqual(country_nl._state.adding, country_be._state.adding)
  243. self.assertEqual(country_nl._state.db, country_be._state.db)
  244. def test_set_state_with_pk_specified(self):
  245. state_ca = State(two_letter_code='CA')
  246. state_ny = State(two_letter_code='NY')
  247. State.objects.bulk_create([state_ca])
  248. state_ny.save()
  249. # Objects save via bulk_create() and save() should have equal state.
  250. self.assertEqual(state_ca._state.adding, state_ny._state.adding)
  251. self.assertEqual(state_ca._state.db, state_ny._state.db)
  252. @skipIfDBFeature('supports_ignore_conflicts')
  253. def test_ignore_conflicts_value_error(self):
  254. message = 'This database backend does not support ignoring conflicts.'
  255. with self.assertRaisesMessage(NotSupportedError, message):
  256. TwoFields.objects.bulk_create(self.data, ignore_conflicts=True)
  257. @skipUnlessDBFeature('supports_ignore_conflicts')
  258. def test_ignore_conflicts_ignore(self):
  259. data = [
  260. TwoFields(f1=1, f2=1),
  261. TwoFields(f1=2, f2=2),
  262. TwoFields(f1=3, f2=3),
  263. ]
  264. TwoFields.objects.bulk_create(data)
  265. self.assertEqual(TwoFields.objects.count(), 3)
  266. # With ignore_conflicts=True, conflicts are ignored.
  267. conflicting_objects = [
  268. TwoFields(f1=2, f2=2),
  269. TwoFields(f1=3, f2=3),
  270. ]
  271. TwoFields.objects.bulk_create([conflicting_objects[0]], ignore_conflicts=True)
  272. TwoFields.objects.bulk_create(conflicting_objects, ignore_conflicts=True)
  273. self.assertEqual(TwoFields.objects.count(), 3)
  274. self.assertIsNone(conflicting_objects[0].pk)
  275. self.assertIsNone(conflicting_objects[1].pk)
  276. # New objects are created and conflicts are ignored.
  277. new_object = TwoFields(f1=4, f2=4)
  278. TwoFields.objects.bulk_create(conflicting_objects + [new_object], ignore_conflicts=True)
  279. self.assertEqual(TwoFields.objects.count(), 4)
  280. self.assertIsNone(new_object.pk)
  281. # Without ignore_conflicts=True, there's a problem.
  282. with self.assertRaises(IntegrityError):
  283. TwoFields.objects.bulk_create(conflicting_objects)