Browse Source

Fixed #14094 -- Added support for unlimited CharField on PostgreSQL.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Adrian Torres 2 years ago
parent
commit
7eee1dca42

+ 2 - 1
django/core/management/commands/inspectdb.py

@@ -339,7 +339,8 @@ class Command(BaseCommand):
 
         # Add max_length for all CharFields.
         if field_type == "CharField" and row.display_size:
-            field_params["max_length"] = int(row.display_size)
+            if (size := int(row.display_size)) and size > 0:
+                field_params["max_length"] = size
 
         if field_type in {"CharField", "TextField"} and row.collation:
             field_params["db_collation"] = row.collation

+ 3 - 0
django/db/backends/base/features.py

@@ -345,6 +345,9 @@ class BaseDatabaseFeatures:
     # Set to (exception, message) if null characters in text are disallowed.
     prohibits_null_characters_in_text_exception = None
 
+    # Does the backend support unlimited character columns?
+    supports_unlimited_charfield = False
+
     # Collation names for use by the Django test suite.
     test_collations = {
         "ci": None,  # Case-insensitive.

+ 7 - 1
django/db/backends/postgresql/base.py

@@ -80,6 +80,12 @@ from .operations import DatabaseOperations  # NOQA isort:skip
 from .schema import DatabaseSchemaEditor  # NOQA isort:skip
 
 
+def _get_varchar_column(data):
+    if data["max_length"] is None:
+        return "varchar"
+    return "varchar(%(max_length)s)" % data
+
+
 class DatabaseWrapper(BaseDatabaseWrapper):
     vendor = "postgresql"
     display_name = "PostgreSQL"
@@ -92,7 +98,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         "BigAutoField": "bigint",
         "BinaryField": "bytea",
         "BooleanField": "boolean",
-        "CharField": "varchar(%(max_length)s)",
+        "CharField": _get_varchar_column,
         "DateField": "date",
         "DateTimeField": "timestamp with time zone",
         "DecimalField": "numeric(%(max_digits)s, %(decimal_places)s)",

+ 1 - 0
django/db/backends/postgresql/features.py

@@ -110,3 +110,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
 
     has_bit_xor = property(operator.attrgetter("is_postgresql_14"))
     supports_covering_spgist_indexes = property(operator.attrgetter("is_postgresql_14"))
+    supports_unlimited_charfield = True

+ 19 - 3
django/db/models/fields/__init__.py

@@ -817,9 +817,14 @@ class Field(RegisterLookupMixin):
         # exactly which wacky database column type you want to use.
         data = self.db_type_parameters(connection)
         try:
-            return connection.data_types[self.get_internal_type()] % data
+            column_type = connection.data_types[self.get_internal_type()]
         except KeyError:
             return None
+        else:
+            # column_type is either a single-parameter function or a string.
+            if callable(column_type):
+                return column_type(data)
+            return column_type % data
 
     def rel_db_type(self, connection):
         """
@@ -1130,14 +1135,19 @@ class BooleanField(Field):
 
 
 class CharField(Field):
-    description = _("String (up to %(max_length)s)")
-
     def __init__(self, *args, db_collation=None, **kwargs):
         super().__init__(*args, **kwargs)
         self.db_collation = db_collation
         if self.max_length is not None:
             self.validators.append(validators.MaxLengthValidator(self.max_length))
 
+    @property
+    def description(self):
+        if self.max_length is not None:
+            return _("String (up to %(max_length)s)")
+        else:
+            return _("String (unlimited)")
+
     def check(self, **kwargs):
         databases = kwargs.get("databases") or []
         return [
@@ -1148,6 +1158,12 @@ class CharField(Field):
 
     def _check_max_length_attribute(self, **kwargs):
         if self.max_length is None:
+            if (
+                connection.features.supports_unlimited_charfield
+                or "supports_unlimited_charfield"
+                in self.model._meta.required_db_features
+            ):
+                return []
             return [
                 checks.Error(
                     "CharFields must define a 'max_length' attribute.",

+ 8 - 2
docs/ref/models/fields.txt

@@ -617,9 +617,11 @@ The default form widget for this field is a :class:`~django.forms.TextInput`.
 
 .. attribute:: CharField.max_length
 
-    Required. The maximum length (in characters) of the field. The max_length
+    The maximum length (in characters) of the field. The ``max_length``
     is enforced at the database level and in Django's validation using
-    :class:`~django.core.validators.MaxLengthValidator`.
+    :class:`~django.core.validators.MaxLengthValidator`. It's required for all
+    database backends included with Django except PostgreSQL, which supports
+    unlimited ``VARCHAR`` columns.
 
     .. note::
 
@@ -628,6 +630,10 @@ The default form widget for this field is a :class:`~django.forms.TextInput`.
         ``max_length`` for some backends. Refer to the :doc:`database backend
         notes </ref/databases>` for details.
 
+    .. versionchanged:: 4.2
+
+        Support for unlimited ``VARCHAR`` columns was added on PostgreSQL.
+
 .. attribute:: CharField.db_collation
 
     Optional. The database collation name of the field.

+ 4 - 0
docs/releases/4.2.txt

@@ -318,6 +318,10 @@ Models
   :meth:`~.RelatedManager.aclear`, :meth:`~.RelatedManager.aremove`, and
   :meth:`~.RelatedManager.aset`.
 
+* :attr:`CharField.max_length <django.db.models.CharField.max_length>` is no
+  longer required to be set on PostgreSQL, which supports unlimited ``VARCHAR``
+  columns.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 10 - 0
tests/admin_docs/test_views.py

@@ -447,6 +447,16 @@ class TestFieldType(unittest.TestCase):
             "Boolean (Either True or False)",
         )
 
+    def test_char_fields(self):
+        self.assertEqual(
+            views.get_readable_field_data_type(fields.CharField(max_length=255)),
+            "String (up to 255)",
+        )
+        self.assertEqual(
+            views.get_readable_field_data_type(fields.CharField()),
+            "String (unlimited)",
+        )
+
     def test_custom_fields(self):
         self.assertEqual(
             views.get_readable_field_data_type(CustomField()), "A custom field type"

+ 7 - 0
tests/inspectdb/models.py

@@ -106,6 +106,13 @@ class TextFieldDbCollation(models.Model):
         required_db_features = {"supports_collation_on_textfield"}
 
 
+class CharFieldUnlimited(models.Model):
+    char_field = models.CharField(max_length=None)
+
+    class Meta:
+        required_db_features = {"supports_unlimited_charfield"}
+
+
 class UniqueTogether(models.Model):
     field1 = models.IntegerField()
     field2 = models.CharField(max_length=10)

+ 7 - 0
tests/inspectdb/tests.py

@@ -184,6 +184,13 @@ class InspectDBTestCase(TestCase):
                 output,
             )
 
+    @skipUnlessDBFeature("supports_unlimited_charfield")
+    def test_char_field_unlimited(self):
+        out = StringIO()
+        call_command("inspectdb", "inspectdb_charfieldunlimited", stdout=out)
+        output = out.getvalue()
+        self.assertIn("char_field = models.CharField()", output)
+
     def test_number_field_types(self):
         """Test introspection of various Django field types"""
         assertFieldType = self.make_field_type_asserter()

+ 6 - 4
tests/invalid_models_tests/test_ordinary_fields.py

@@ -112,16 +112,18 @@ class CharFieldTests(TestCase):
             field = models.CharField()
 
         field = Model._meta.get_field("field")
-        self.assertEqual(
-            field.check(),
-            [
+        expected = (
+            []
+            if connection.features.supports_unlimited_charfield
+            else [
                 Error(
                     "CharFields must define a 'max_length' attribute.",
                     obj=field,
                     id="fields.E120",
                 ),
-            ],
+            ]
         )
+        self.assertEqual(field.check(), expected)
 
     def test_negative_max_length(self):
         class Model(models.Model):

+ 4 - 4
tests/postgres_tests/test_array.py

@@ -776,12 +776,12 @@ class TestOtherTypesExactQuerying(PostgreSQLTestCase):
 class TestChecks(PostgreSQLSimpleTestCase):
     def test_field_checks(self):
         class MyModel(PostgreSQLModel):
-            field = ArrayField(models.CharField())
+            field = ArrayField(models.CharField(max_length=-1))
 
         model = MyModel()
         errors = model.check()
         self.assertEqual(len(errors), 1)
-        # The inner CharField is missing a max_length.
+        # The inner CharField has a non-positive max_length.
         self.assertEqual(errors[0].id, "postgres.E001")
         self.assertIn("max_length", errors[0].msg)
 
@@ -837,12 +837,12 @@ class TestChecks(PostgreSQLSimpleTestCase):
         """
 
         class MyModel(PostgreSQLModel):
-            field = ArrayField(ArrayField(models.CharField()))
+            field = ArrayField(ArrayField(models.CharField(max_length=-1)))
 
         model = MyModel()
         errors = model.check()
         self.assertEqual(len(errors), 1)
-        # The inner CharField is missing a max_length.
+        # The inner CharField has a non-positive max_length.
         self.assertEqual(errors[0].id, "postgres.E001")
         self.assertIn("max_length", errors[0].msg)