From 7eee1dca420ee7c4451d24cd32fa08aed0dcbb36 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Thu, 10 Nov 2022 19:31:31 +0100 Subject: [PATCH] Fixed #14094 -- Added support for unlimited CharField on PostgreSQL. Co-authored-by: Mariusz Felisiak --- django/core/management/commands/inspectdb.py | 3 ++- django/db/backends/base/features.py | 3 +++ django/db/backends/postgresql/base.py | 8 ++++++- django/db/backends/postgresql/features.py | 1 + django/db/models/fields/__init__.py | 22 ++++++++++++++++--- docs/ref/models/fields.txt | 10 +++++++-- docs/releases/4.2.txt | 4 ++++ tests/admin_docs/test_views.py | 10 +++++++++ tests/inspectdb/models.py | 7 ++++++ tests/inspectdb/tests.py | 7 ++++++ .../test_ordinary_fields.py | 10 +++++---- tests/postgres_tests/test_array.py | 8 +++---- 12 files changed, 78 insertions(+), 15 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 51d7866503..992c523a8e 100644 --- a/django/core/management/commands/inspectdb.py +++ b/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 diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index b5b6c5b55d..ef1fe88336 100644 --- a/django/db/backends/base/features.py +++ b/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. diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index ceea1bebad..99403f5322 100644 --- a/django/db/backends/postgresql/base.py +++ b/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)", diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 49658cd267..6c20dd87f0 100644 --- a/django/db/backends/postgresql/features.py +++ b/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 diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index e3b47d173c..060e1be605 100644 --- a/django/db/models/fields/__init__.py +++ b/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.", diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index d460e81ef4..4fb39fde82 100644 --- a/docs/ref/models/fields.txt +++ b/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 ` 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. diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 90fdf9bd1b..755ebcb717 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -318,6 +318,10 @@ Models :meth:`~.RelatedManager.aclear`, :meth:`~.RelatedManager.aremove`, and :meth:`~.RelatedManager.aset`. +* :attr:`CharField.max_length ` is no + longer required to be set on PostgreSQL, which supports unlimited ``VARCHAR`` + columns. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_docs/test_views.py b/tests/admin_docs/test_views.py index d85f401855..29bbe40310 100644 --- a/tests/admin_docs/test_views.py +++ b/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" diff --git a/tests/inspectdb/models.py b/tests/inspectdb/models.py index 5db4d3bc73..9e6871ce46 100644 --- a/tests/inspectdb/models.py +++ b/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) diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 2f26814625..ad929fd9bc 100644 --- a/tests/inspectdb/tests.py +++ b/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() diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py index 4e07c95956..4e37c48286 100644 --- a/tests/invalid_models_tests/test_ordinary_fields.py +++ b/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): diff --git a/tests/postgres_tests/test_array.py b/tests/postgres_tests/test_array.py index 86e9d00b41..4808f88689 100644 --- a/tests/postgres_tests/test_array.py +++ b/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)