test_generatedfield.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. from django.db import IntegrityError, connection
  2. from django.db.models import (
  3. CharField,
  4. F,
  5. FloatField,
  6. GeneratedField,
  7. IntegerField,
  8. Model,
  9. )
  10. from django.db.models.functions import Lower
  11. from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
  12. from django.test.utils import isolate_apps
  13. from .models import (
  14. GeneratedModel,
  15. GeneratedModelNull,
  16. GeneratedModelNullVirtual,
  17. GeneratedModelOutputFieldDbCollation,
  18. GeneratedModelOutputFieldDbCollationVirtual,
  19. GeneratedModelParams,
  20. GeneratedModelParamsVirtual,
  21. GeneratedModelVirtual,
  22. )
  23. class BaseGeneratedFieldTests(SimpleTestCase):
  24. def test_editable_unsupported(self):
  25. with self.assertRaisesMessage(ValueError, "GeneratedField cannot be editable."):
  26. GeneratedField(
  27. expression=Lower("name"),
  28. output_field=CharField(max_length=255),
  29. editable=True,
  30. db_persist=False,
  31. )
  32. def test_blank_unsupported(self):
  33. with self.assertRaisesMessage(ValueError, "GeneratedField must be blank."):
  34. GeneratedField(
  35. expression=Lower("name"),
  36. output_field=CharField(max_length=255),
  37. blank=False,
  38. db_persist=False,
  39. )
  40. def test_default_unsupported(self):
  41. msg = "GeneratedField cannot have a default."
  42. with self.assertRaisesMessage(ValueError, msg):
  43. GeneratedField(
  44. expression=Lower("name"),
  45. output_field=CharField(max_length=255),
  46. default="",
  47. db_persist=False,
  48. )
  49. def test_database_default_unsupported(self):
  50. msg = "GeneratedField cannot have a database default."
  51. with self.assertRaisesMessage(ValueError, msg):
  52. GeneratedField(
  53. expression=Lower("name"),
  54. output_field=CharField(max_length=255),
  55. db_default="",
  56. db_persist=False,
  57. )
  58. def test_db_persist_required(self):
  59. msg = "GeneratedField.db_persist must be True or False."
  60. with self.assertRaisesMessage(ValueError, msg):
  61. GeneratedField(
  62. expression=Lower("name"), output_field=CharField(max_length=255)
  63. )
  64. with self.assertRaisesMessage(ValueError, msg):
  65. GeneratedField(
  66. expression=Lower("name"),
  67. output_field=CharField(max_length=255),
  68. db_persist=None,
  69. )
  70. def test_deconstruct(self):
  71. field = GeneratedField(
  72. expression=F("a") + F("b"), output_field=IntegerField(), db_persist=True
  73. )
  74. _, path, args, kwargs = field.deconstruct()
  75. self.assertEqual(path, "django.db.models.GeneratedField")
  76. self.assertEqual(args, [])
  77. self.assertEqual(kwargs["db_persist"], True)
  78. self.assertEqual(kwargs["expression"], F("a") + F("b"))
  79. self.assertEqual(
  80. kwargs["output_field"].deconstruct(), IntegerField().deconstruct()
  81. )
  82. @isolate_apps("model_fields")
  83. def test_get_col(self):
  84. class Square(Model):
  85. side = IntegerField()
  86. area = GeneratedField(
  87. expression=F("side") * F("side"),
  88. output_field=IntegerField(),
  89. db_persist=True,
  90. )
  91. col = Square._meta.get_field("area").get_col("alias")
  92. self.assertIsInstance(col.output_field, IntegerField)
  93. class FloatSquare(Model):
  94. side = IntegerField()
  95. area = GeneratedField(
  96. expression=F("side") * F("side"),
  97. db_persist=True,
  98. output_field=FloatField(),
  99. )
  100. col = FloatSquare._meta.get_field("area").get_col("alias")
  101. self.assertIsInstance(col.output_field, FloatField)
  102. @isolate_apps("model_fields")
  103. def test_cached_col(self):
  104. class Sum(Model):
  105. a = IntegerField()
  106. b = IntegerField()
  107. total = GeneratedField(
  108. expression=F("a") + F("b"), output_field=IntegerField(), db_persist=True
  109. )
  110. field = Sum._meta.get_field("total")
  111. cached_col = field.cached_col
  112. self.assertIs(field.get_col(Sum._meta.db_table), cached_col)
  113. self.assertIs(field.get_col(Sum._meta.db_table, field), cached_col)
  114. self.assertIsNot(field.get_col("alias"), cached_col)
  115. self.assertIsNot(field.get_col(Sum._meta.db_table, IntegerField()), cached_col)
  116. self.assertIs(cached_col.target, field)
  117. self.assertIsInstance(cached_col.output_field, IntegerField)
  118. class GeneratedFieldTestMixin:
  119. def _refresh_if_needed(self, m):
  120. if not connection.features.can_return_columns_from_insert:
  121. m.refresh_from_db()
  122. return m
  123. def test_unsaved_error(self):
  124. m = self.base_model(a=1, b=2)
  125. msg = "Cannot read a generated field from an unsaved model."
  126. with self.assertRaisesMessage(AttributeError, msg):
  127. m.field
  128. def test_create(self):
  129. m = self.base_model.objects.create(a=1, b=2)
  130. m = self._refresh_if_needed(m)
  131. self.assertEqual(m.field, 3)
  132. def test_non_nullable_create(self):
  133. with self.assertRaises(IntegrityError):
  134. self.base_model.objects.create()
  135. def test_save(self):
  136. # Insert.
  137. m = self.base_model(a=2, b=4)
  138. m.save()
  139. m = self._refresh_if_needed(m)
  140. self.assertEqual(m.field, 6)
  141. # Update.
  142. m.a = 4
  143. m.save()
  144. m.refresh_from_db()
  145. self.assertEqual(m.field, 8)
  146. def test_update(self):
  147. m = self.base_model.objects.create(a=1, b=2)
  148. self.base_model.objects.update(b=3)
  149. m = self.base_model.objects.get(pk=m.pk)
  150. self.assertEqual(m.field, 4)
  151. def test_bulk_create(self):
  152. m = self.base_model(a=3, b=4)
  153. (m,) = self.base_model.objects.bulk_create([m])
  154. if not connection.features.can_return_rows_from_bulk_insert:
  155. m = self.base_model.objects.get()
  156. self.assertEqual(m.field, 7)
  157. def test_bulk_update(self):
  158. m = self.base_model.objects.create(a=1, b=2)
  159. m.a = 3
  160. self.base_model.objects.bulk_update([m], fields=["a"])
  161. m = self.base_model.objects.get(pk=m.pk)
  162. self.assertEqual(m.field, 5)
  163. def test_output_field_lookups(self):
  164. """Lookups from the output_field are available on GeneratedFields."""
  165. internal_type = IntegerField().get_internal_type()
  166. min_value, max_value = connection.ops.integer_field_range(internal_type)
  167. if min_value is None:
  168. self.skipTest("Backend doesn't define an integer min value.")
  169. if max_value is None:
  170. self.skipTest("Backend doesn't define an integer max value.")
  171. does_not_exist = self.base_model.DoesNotExist
  172. underflow_value = min_value - 1
  173. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  174. self.base_model.objects.get(field=underflow_value)
  175. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  176. self.base_model.objects.get(field__lt=underflow_value)
  177. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  178. self.base_model.objects.get(field__lte=underflow_value)
  179. overflow_value = max_value + 1
  180. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  181. self.base_model.objects.get(field=overflow_value)
  182. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  183. self.base_model.objects.get(field__gt=overflow_value)
  184. with self.assertNumQueries(0), self.assertRaises(does_not_exist):
  185. self.base_model.objects.get(field__gte=overflow_value)
  186. def test_output_field_db_collation(self):
  187. collation = connection.features.test_collations["virtual"]
  188. m = self.output_field_db_collation_model.objects.create(name="NAME")
  189. field = m._meta.get_field("lower_name")
  190. db_parameters = field.db_parameters(connection)
  191. self.assertEqual(db_parameters["collation"], collation)
  192. self.assertEqual(db_parameters["type"], field.output_field.db_type(connection))
  193. self.assertNotEqual(
  194. db_parameters["type"],
  195. field._resolved_expression.output_field.db_type(connection),
  196. )
  197. def test_db_type_parameters(self):
  198. db_type_parameters = self.output_field_db_collation_model._meta.get_field(
  199. "lower_name"
  200. ).db_type_parameters(connection)
  201. self.assertEqual(db_type_parameters["max_length"], 11)
  202. def test_model_with_params(self):
  203. m = self.params_model.objects.create()
  204. m = self._refresh_if_needed(m)
  205. self.assertEqual(m.field, "Constant")
  206. def test_nullable(self):
  207. m1 = self.nullable_model.objects.create()
  208. m1 = self._refresh_if_needed(m1)
  209. none_val = "" if connection.features.interprets_empty_strings_as_nulls else None
  210. self.assertEqual(m1.lower_name, none_val)
  211. m2 = self.nullable_model.objects.create(name="NaMe")
  212. m2 = self._refresh_if_needed(m2)
  213. self.assertEqual(m2.lower_name, "name")
  214. @skipUnlessDBFeature("supports_stored_generated_columns")
  215. class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
  216. base_model = GeneratedModel
  217. nullable_model = GeneratedModelNull
  218. output_field_db_collation_model = GeneratedModelOutputFieldDbCollation
  219. params_model = GeneratedModelParams
  220. @skipUnlessDBFeature("supports_virtual_generated_columns")
  221. class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
  222. base_model = GeneratedModelVirtual
  223. nullable_model = GeneratedModelNullVirtual
  224. output_field_db_collation_model = GeneratedModelOutputFieldDbCollationVirtual
  225. params_model = GeneratedModelParamsVirtual