summaryrefslogtreecommitdiff
path: root/tests/model_fields
diff options
context:
space:
mode:
authorJeremy Nauta <jeremypnauta@gmail.com>2023-07-06 20:36:48 -0600
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-09-07 06:19:08 +0200
commitf333e3513e8bdf5ffeb6eeb63021c230082e6f95 (patch)
tree45840089369287128724be0a63e3af36f3178469 /tests/model_fields
parentcafe7266ee69f7e017ddbc0d440084ace559b04b (diff)
Fixed #31300 -- Added GeneratedField model field.
Thanks Adam Johnson and Paolo Melchiorre for reviews. Co-Authored-By: Lily Foote <code@lilyf.org> Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Diffstat (limited to 'tests/model_fields')
-rw-r--r--tests/model_fields/models.py98
-rw-r--r--tests/model_fields/test_generatedfield.py176
2 files changed, 273 insertions, 1 deletions
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index e1a5a3872f..7fb0f8b610 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -6,8 +6,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
from django.contrib.contenttypes.models import ContentType
from django.core.files.storage import FileSystemStorage
from django.core.serializers.json import DjangoJSONEncoder
-from django.db import models
+from django.db import connection, models
+from django.db.models import F, Value
from django.db.models.fields.files import ImageFieldFile
+from django.db.models.functions import Lower
+from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext_lazy as _
try:
@@ -16,6 +19,11 @@ except ImportError:
Image = None
+test_collation = SimpleLazyObject(
+ lambda: connection.features.test_collations.get("non_default")
+)
+
+
class Foo(models.Model):
a = models.CharField(max_length=10)
d = models.DecimalField(max_digits=5, decimal_places=3)
@@ -468,3 +476,91 @@ class UUIDChild(PrimaryKeyUUIDModel):
class UUIDGrandchild(UUIDChild):
pass
+
+
+class GeneratedModel(models.Model):
+ a = models.IntegerField()
+ b = models.IntegerField()
+ field = models.GeneratedField(expression=F("a") + F("b"), db_persist=True)
+
+ class Meta:
+ required_db_features = {"supports_stored_generated_columns"}
+
+
+class GeneratedModelVirtual(models.Model):
+ a = models.IntegerField()
+ b = models.IntegerField()
+ field = models.GeneratedField(expression=F("a") + F("b"), db_persist=False)
+
+ class Meta:
+ required_db_features = {"supports_virtual_generated_columns"}
+
+
+class GeneratedModelParams(models.Model):
+ field = models.GeneratedField(
+ expression=Value("Constant", output_field=models.CharField(max_length=10)),
+ db_persist=True,
+ )
+
+ class Meta:
+ required_db_features = {"supports_stored_generated_columns"}
+
+
+class GeneratedModelParamsVirtual(models.Model):
+ field = models.GeneratedField(
+ expression=Value("Constant", output_field=models.CharField(max_length=10)),
+ db_persist=False,
+ )
+
+ class Meta:
+ required_db_features = {"supports_virtual_generated_columns"}
+
+
+class GeneratedModelOutputField(models.Model):
+ name = models.CharField(max_length=10)
+ lower_name = models.GeneratedField(
+ expression=Lower("name"),
+ output_field=models.CharField(db_collation=test_collation, max_length=11),
+ db_persist=True,
+ )
+
+ class Meta:
+ required_db_features = {
+ "supports_stored_generated_columns",
+ "supports_collation_on_charfield",
+ }
+
+
+class GeneratedModelOutputFieldVirtual(models.Model):
+ name = models.CharField(max_length=10)
+ lower_name = models.GeneratedField(
+ expression=Lower("name"),
+ db_persist=False,
+ output_field=models.CharField(db_collation=test_collation, max_length=11),
+ )
+
+ class Meta:
+ required_db_features = {
+ "supports_virtual_generated_columns",
+ "supports_collation_on_charfield",
+ }
+
+
+class GeneratedModelNull(models.Model):
+ name = models.CharField(max_length=10, null=True)
+ lower_name = models.GeneratedField(
+ expression=Lower("name"), db_persist=True, null=True
+ )
+
+ class Meta:
+ required_db_features = {"supports_stored_generated_columns"}
+
+
+class GeneratedModelNullVirtual(models.Model):
+ name = models.CharField(max_length=10, null=True)
+ lower_name = models.GeneratedField(
+ expression=Lower("name"), db_persist=False, null=True
+ )
+
+ class Meta:
+ required_db_features = {"supports_virtual_generated_columns"}
diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py
new file mode 100644
index 0000000000..e2746bdd0c
--- /dev/null
+++ b/tests/model_fields/test_generatedfield.py
@@ -0,0 +1,176 @@
+from django.core.exceptions import FieldError
+from django.db import IntegrityError, connection
+from django.db.models import F, GeneratedField, IntegerField
+from django.db.models.functions import Lower
+from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
+
+from .models import (
+ GeneratedModel,
+ GeneratedModelNull,
+ GeneratedModelNullVirtual,
+ GeneratedModelOutputField,
+ GeneratedModelOutputFieldVirtual,
+ GeneratedModelParams,
+ GeneratedModelParamsVirtual,
+ GeneratedModelVirtual,
+)
+
+
+class BaseGeneratedFieldTests(SimpleTestCase):
+ def test_editable_unsupported(self):
+ with self.assertRaisesMessage(ValueError, "GeneratedField cannot be editable."):
+ GeneratedField(expression=Lower("name"), editable=True, db_persist=False)
+
+ def test_blank_unsupported(self):
+ with self.assertRaisesMessage(ValueError, "GeneratedField must be blank."):
+ GeneratedField(expression=Lower("name"), blank=False, db_persist=False)
+
+ def test_default_unsupported(self):
+ msg = "GeneratedField cannot have a default."
+ with self.assertRaisesMessage(ValueError, msg):
+ GeneratedField(expression=Lower("name"), default="", db_persist=False)
+
+ def test_database_default_unsupported(self):
+ msg = "GeneratedField cannot have a database default."
+ with self.assertRaisesMessage(ValueError, msg):
+ GeneratedField(expression=Lower("name"), db_default="", db_persist=False)
+
+ def test_db_persist_required(self):
+ msg = "GeneratedField.db_persist must be True or False."
+ with self.assertRaisesMessage(ValueError, msg):
+ GeneratedField(expression=Lower("name"))
+ with self.assertRaisesMessage(ValueError, msg):
+ GeneratedField(expression=Lower("name"), db_persist=None)
+
+ def test_deconstruct(self):
+ field = GeneratedField(expression=F("a") + F("b"), db_persist=True)
+ _, path, args, kwargs = field.deconstruct()
+ self.assertEqual(path, "django.db.models.GeneratedField")
+ self.assertEqual(args, [])
+ self.assertEqual(kwargs, {"db_persist": True, "expression": F("a") + F("b")})
+
+
+class GeneratedFieldTestMixin:
+ def _refresh_if_needed(self, m):
+ if not connection.features.can_return_columns_from_insert:
+ m.refresh_from_db()
+ return m
+
+ def test_unsaved_error(self):
+ m = self.base_model(a=1, b=2)
+ msg = "Cannot read a generated field from an unsaved model."
+ with self.assertRaisesMessage(FieldError, msg):
+ m.field
+
+ def test_create(self):
+ m = self.base_model.objects.create(a=1, b=2)
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.field, 3)
+
+ def test_non_nullable_create(self):
+ with self.assertRaises(IntegrityError):
+ self.base_model.objects.create()
+
+ def test_save(self):
+ # Insert.
+ m = self.base_model(a=2, b=4)
+ m.save()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.field, 6)
+ # Update.
+ m.a = 4
+ m.save()
+ m.refresh_from_db()
+ self.assertEqual(m.field, 8)
+
+ def test_update(self):
+ m = self.base_model.objects.create(a=1, b=2)
+ self.base_model.objects.update(b=3)
+ m = self.base_model.objects.get(pk=m.pk)
+ self.assertEqual(m.field, 4)
+
+ def test_bulk_create(self):
+ m = self.base_model(a=3, b=4)
+ (m,) = self.base_model.objects.bulk_create([m])
+ if not connection.features.can_return_rows_from_bulk_insert:
+ m = self.base_model.objects.get()
+ self.assertEqual(m.field, 7)
+
+ def test_bulk_update(self):
+ m = self.base_model.objects.create(a=1, b=2)
+ m.a = 3
+ self.base_model.objects.bulk_update([m], fields=["a"])
+ m = self.base_model.objects.get(pk=m.pk)
+ self.assertEqual(m.field, 5)
+
+ def test_output_field_lookups(self):
+ """Lookups from the output_field are available on GeneratedFields."""
+ internal_type = IntegerField().get_internal_type()
+ min_value, max_value = connection.ops.integer_field_range(internal_type)
+ if min_value is None:
+ self.skipTest("Backend doesn't define an integer min value.")
+ if max_value is None:
+ self.skipTest("Backend doesn't define an integer max value.")
+
+ does_not_exist = self.base_model.DoesNotExist
+ underflow_value = min_value - 1
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field=underflow_value)
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field__lt=underflow_value)
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field__lte=underflow_value)
+
+ overflow_value = max_value + 1
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field=overflow_value)
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field__gt=overflow_value)
+ with self.assertNumQueries(0), self.assertRaises(does_not_exist):
+ self.base_model.objects.get(field__gte=overflow_value)
+
+ @skipUnlessDBFeature("supports_collation_on_charfield")
+ def test_output_field(self):
+ collation = connection.features.test_collations.get("non_default")
+ if not collation:
+ self.skipTest("Language collations are not supported.")
+
+ m = self.output_field_model.objects.create(name="NAME")
+ field = m._meta.get_field("lower_name")
+ db_parameters = field.db_parameters(connection)
+ self.assertEqual(db_parameters["collation"], collation)
+ self.assertEqual(db_parameters["type"], field.output_field.db_type(connection))
+ self.assertNotEqual(
+ db_parameters["type"],
+ field._resolved_expression.output_field.db_type(connection),
+ )
+
+ def test_model_with_params(self):
+ m = self.params_model.objects.create()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.field, "Constant")
+
+ def test_nullable(self):
+ m1 = self.nullable_model.objects.create()
+ m1 = self._refresh_if_needed(m1)
+ none_val = "" if connection.features.interprets_empty_strings_as_nulls else None
+ self.assertEqual(m1.lower_name, none_val)
+ m2 = self.nullable_model.objects.create(name="NaMe")
+ m2 = self._refresh_if_needed(m2)
+ self.assertEqual(m2.lower_name, "name")
+
+
+@skipUnlessDBFeature("supports_stored_generated_columns")
+class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
+ base_model = GeneratedModel
+ nullable_model = GeneratedModelNull
+ output_field_model = GeneratedModelOutputField
+ params_model = GeneratedModelParams
+
+
+@skipUnlessDBFeature("supports_virtual_generated_columns")
+class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
+ base_model = GeneratedModelVirtual
+ nullable_model = GeneratedModelNullVirtual
+ output_field_model = GeneratedModelOutputFieldVirtual
+ params_model = GeneratedModelParamsVirtual