1
0
mirror of https://github.com/django/django.git synced 2024-12-23 01:25:58 +00:00

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

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
Adrian Torres 2022-11-10 19:31:31 +01:00 committed by Mariusz Felisiak
parent 78f163a4fb
commit 7eee1dca42
12 changed files with 78 additions and 15 deletions

View File

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

View File

@ -345,6 +345,9 @@ class BaseDatabaseFeatures:
# Set to (exception, message) if null characters in text are disallowed. # Set to (exception, message) if null characters in text are disallowed.
prohibits_null_characters_in_text_exception = None 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. # Collation names for use by the Django test suite.
test_collations = { test_collations = {
"ci": None, # Case-insensitive. "ci": None, # Case-insensitive.

View File

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

View File

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

View File

@ -817,9 +817,14 @@ class Field(RegisterLookupMixin):
# exactly which wacky database column type you want to use. # exactly which wacky database column type you want to use.
data = self.db_type_parameters(connection) data = self.db_type_parameters(connection)
try: try:
return connection.data_types[self.get_internal_type()] % data column_type = connection.data_types[self.get_internal_type()]
except KeyError: except KeyError:
return None 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): def rel_db_type(self, connection):
""" """
@ -1130,14 +1135,19 @@ class BooleanField(Field):
class CharField(Field): class CharField(Field):
description = _("String (up to %(max_length)s)")
def __init__(self, *args, db_collation=None, **kwargs): def __init__(self, *args, db_collation=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_collation = db_collation self.db_collation = db_collation
if self.max_length is not None: if self.max_length is not None:
self.validators.append(validators.MaxLengthValidator(self.max_length)) 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): def check(self, **kwargs):
databases = kwargs.get("databases") or [] databases = kwargs.get("databases") or []
return [ return [
@ -1148,6 +1158,12 @@ class CharField(Field):
def _check_max_length_attribute(self, **kwargs): def _check_max_length_attribute(self, **kwargs):
if self.max_length is None: 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 [ return [
checks.Error( checks.Error(
"CharFields must define a 'max_length' attribute.", "CharFields must define a 'max_length' attribute.",

View File

@ -617,9 +617,11 @@ The default form widget for this field is a :class:`~django.forms.TextInput`.
.. attribute:: CharField.max_length .. 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 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:: .. 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 ``max_length`` for some backends. Refer to the :doc:`database backend
notes </ref/databases>` for details. notes </ref/databases>` for details.
.. versionchanged:: 4.2
Support for unlimited ``VARCHAR`` columns was added on PostgreSQL.
.. attribute:: CharField.db_collation .. attribute:: CharField.db_collation
Optional. The database collation name of the field. Optional. The database collation name of the field.

View File

@ -318,6 +318,10 @@ Models
:meth:`~.RelatedManager.aclear`, :meth:`~.RelatedManager.aremove`, and :meth:`~.RelatedManager.aclear`, :meth:`~.RelatedManager.aremove`, and
:meth:`~.RelatedManager.aset`. :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 Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

@ -447,6 +447,16 @@ class TestFieldType(unittest.TestCase):
"Boolean (Either True or False)", "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): def test_custom_fields(self):
self.assertEqual( self.assertEqual(
views.get_readable_field_data_type(CustomField()), "A custom field type" views.get_readable_field_data_type(CustomField()), "A custom field type"

View File

@ -106,6 +106,13 @@ class TextFieldDbCollation(models.Model):
required_db_features = {"supports_collation_on_textfield"} 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): class UniqueTogether(models.Model):
field1 = models.IntegerField() field1 = models.IntegerField()
field2 = models.CharField(max_length=10) field2 = models.CharField(max_length=10)

View File

@ -184,6 +184,13 @@ class InspectDBTestCase(TestCase):
output, 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): def test_number_field_types(self):
"""Test introspection of various Django field types""" """Test introspection of various Django field types"""
assertFieldType = self.make_field_type_asserter() assertFieldType = self.make_field_type_asserter()

View File

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

View File

@ -776,12 +776,12 @@ class TestOtherTypesExactQuerying(PostgreSQLTestCase):
class TestChecks(PostgreSQLSimpleTestCase): class TestChecks(PostgreSQLSimpleTestCase):
def test_field_checks(self): def test_field_checks(self):
class MyModel(PostgreSQLModel): class MyModel(PostgreSQLModel):
field = ArrayField(models.CharField()) field = ArrayField(models.CharField(max_length=-1))
model = MyModel() model = MyModel()
errors = model.check() errors = model.check()
self.assertEqual(len(errors), 1) 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.assertEqual(errors[0].id, "postgres.E001")
self.assertIn("max_length", errors[0].msg) self.assertIn("max_length", errors[0].msg)
@ -837,12 +837,12 @@ class TestChecks(PostgreSQLSimpleTestCase):
""" """
class MyModel(PostgreSQLModel): class MyModel(PostgreSQLModel):
field = ArrayField(ArrayField(models.CharField())) field = ArrayField(ArrayField(models.CharField(max_length=-1)))
model = MyModel() model = MyModel()
errors = model.check() errors = model.check()
self.assertEqual(len(errors), 1) 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.assertEqual(errors[0].id, "postgres.E001")
self.assertIn("max_length", errors[0].msg) self.assertIn("max_length", errors[0].msg)