diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 09112cebb6..4b38a2d6b1 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -227,6 +227,9 @@ class BaseDatabaseFeatures: supports_select_difference = True supports_slicing_ordering_in_compound = False + # Does the backend support indexing a TextField? + supports_index_on_text_field = True + def __init__(self, connection): self.connection = connection diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index f32dee09a8..35cc7dfc04 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -143,6 +143,14 @@ class DatabaseWrapper(BaseDatabaseWrapper): else: return self._data_types + # For these columns, MySQL doesn't: + # - accept default values and implicitly treats these columns as nullable + # - support a database index + _limited_data_types = ( + 'tinyblob', 'blob', 'mediumblob', 'longblob', 'tinytext', 'text', + 'mediumtext', 'longtext', 'json', + ) + operators = { 'exact': '= %s', 'iexact': 'LIKE %s', diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index ef02e28bb8..585ea073be 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -31,6 +31,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_select_intersection = False supports_select_difference = False supports_slicing_ordering_in_compound = True + supports_index_on_text_field = False @cached_property def _mysql_storage_engine(self): diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 455fd1b0e3..9cae8b79b5 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -29,20 +29,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): import MySQLdb.converters return MySQLdb.escape(value, MySQLdb.converters.conversions) - def skip_default(self, field): - """ - MySQL doesn't accept default values for some data types and implicitly - treats these columns as nullable. - """ + def _is_limited_data_type(self, field): db_type = field.db_type(self.connection) - return ( - db_type is not None and - db_type.lower() in { - 'tinyblob', 'blob', 'mediumblob', 'longblob', - 'tinytext', 'text', 'mediumtext', 'longtext', - 'json', - } - ) + return db_type is not None and db_type.lower() in self.connection._limited_data_types + + def skip_default(self, field): + return self._is_limited_data_type(field) def add_field(self, model, field): super().add_field(model, field) @@ -69,6 +61,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): field.get_internal_type() == 'ForeignKey' and field.db_constraint): return False + if self._is_limited_data_type(field): + return False return create_index def _delete_composed_index(self, model, fields, *args): diff --git a/django/db/backends/mysql/validation.py b/django/db/backends/mysql/validation.py index b4ae265b03..5cd9a42856 100644 --- a/django/db/backends/mysql/validation.py +++ b/django/db/backends/mysql/validation.py @@ -31,6 +31,7 @@ class DatabaseValidation(BaseDatabaseValidation): MySQL has the following field length restriction: No character (varchar) fields can have a length exceeding 255 characters if they have a unique index on them. + MySQL doesn't support a database index on some data types. """ errors = [] if (field_type.startswith('varchar') and field.unique and @@ -42,4 +43,18 @@ class DatabaseValidation(BaseDatabaseValidation): id='mysql.E001', ) ) + + if field.db_index and field_type.lower() in self.connection._limited_data_types: + errors.append( + checks.Warning( + 'MySQL does not support a database index on %s columns.' + % field_type, + hint=( + "An index won't be created. Silence this warning if " + "you don't care about it." + ), + obj=field, + id='fields.W162', + ) + ) return errors diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index fb49fc5ffb..e2d8f351b0 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -55,6 +55,7 @@ from .introspection import DatabaseIntrospection # NOQA isort:skip from .operations import DatabaseOperations # NOQA isort:skip from .schema import DatabaseSchemaEditor # NOQA isort:skip from .utils import Oracle_datetime # NOQA isort:skip +from .validation import DatabaseValidation # NOQA isort:skip class _UninitializedOperatorsDescriptor: @@ -115,6 +116,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): 'PositiveSmallIntegerField': '%(qn_column)s >= 0', } + # Oracle doesn't support a database index on these columns. + _limited_data_types = ('clob', 'nclob', 'blob') + operators = _UninitializedOperatorsDescriptor() _standard_operators = { @@ -174,6 +178,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): features_class = DatabaseFeatures introspection_class = DatabaseIntrospection ops_class = DatabaseOperations + validation_class = DatabaseValidation def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 3e72a5c29d..af95136fa7 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -35,3 +35,4 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Oracle doesn't ignore quoted identifiers case but the current backend # does by uppercasing all identifiers. ignores_table_name_case = True + supports_index_on_text_field = False diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py index fe85edab17..5b1ae0adcb 100644 --- a/django/db/backends/oracle/schema.py +++ b/django/db/backends/oracle/schema.py @@ -119,3 +119,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): def prepare_default(self, value): return self.quote_value(value) + + def _field_should_be_indexed(self, model, field): + create_index = super()._field_should_be_indexed(model, field) + db_type = field.db_type(self.connection) + if db_type is not None and db_type.lower() in self.connection._limited_data_types: + return False + return create_index diff --git a/django/db/backends/oracle/validation.py b/django/db/backends/oracle/validation.py new file mode 100644 index 0000000000..e5a35fd3ca --- /dev/null +++ b/django/db/backends/oracle/validation.py @@ -0,0 +1,22 @@ +from django.core import checks +from django.db.backends.base.validation import BaseDatabaseValidation + + +class DatabaseValidation(BaseDatabaseValidation): + def check_field_type(self, field, field_type): + """Oracle doesn't support a database index on some data types.""" + errors = [] + if field.db_index and field_type.lower() in self.connection._limited_data_types: + errors.append( + checks.Warning( + 'Oracle does not support a database index on %s columns.' + % field_type, + hint=( + "An index won't be created. Silence this warning if " + "you don't care about it." + ), + obj=field, + id='fields.W162', + ) + ) + return errors diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 570fa7393c..64fd6b68aa 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -172,6 +172,8 @@ Model fields * **fields.E160**: The options ``auto_now``, ``auto_now_add``, and ``default`` are mutually exclusive. Only one of these options may be present. * **fields.W161**: Fixed default value provided. +* **fields.W162**: ```` does not support a database index on + ```` columns. * **fields.E900**: ``IPAddressField`` has been removed except for support in historical migrations. * **fields.W900**: ``IPAddressField`` has been deprecated. Support for it diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py index 90f4b31902..cad375aae5 100644 --- a/tests/invalid_models_tests/test_ordinary_fields.py +++ b/tests/invalid_models_tests/test_ordinary_fields.py @@ -2,7 +2,7 @@ import unittest from django.core.checks import Error, Warning as DjangoWarning from django.db import connection, models -from django.test import SimpleTestCase, TestCase +from django.test import SimpleTestCase, TestCase, skipIfDBFeature from django.test.utils import isolate_apps, override_settings from django.utils.timezone import now @@ -646,3 +646,26 @@ class TimeFieldTests(TestCase): @override_settings(USE_TZ=True) def test_fix_default_value_tz(self): self.test_fix_default_value() + + +@isolate_apps('invalid_models_tests') +class TextFieldTests(TestCase): + + @skipIfDBFeature('supports_index_on_text_field') + def test_max_length_warning(self): + class Model(models.Model): + value = models.TextField(db_index=True) + field = Model._meta.get_field('value') + field_type = field.db_type(connection) + self.assertEqual(field.check(), [ + DjangoWarning( + '%s does not support a database index on %s columns.' + % (connection.display_name, field_type), + hint=( + "An index won't be created. Silence this warning if you " + "don't care about it." + ), + obj=field, + id='fields.W162', + ) + ]) diff --git a/tests/schema/models.py b/tests/schema/models.py index 0c73760d96..0e04dbab87 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -17,6 +17,13 @@ class Author(models.Model): apps = new_apps +class AuthorTextFieldWithIndex(models.Model): + text_field = models.TextField(db_index=True) + + class Meta: + apps = new_apps + + class AuthorWithDefaultHeight(models.Model): name = models.CharField(max_length=255) height = models.PositiveIntegerField(null=True, blank=True, default=42) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 17a4d7c715..ef4b192a36 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -29,11 +29,11 @@ from .fields import ( CustomManyToManyField, InheritedManyToManyField, MediumBlobField, ) from .models import ( - Author, AuthorWithDefaultHeight, AuthorWithEvenLongerName, - AuthorWithIndexedName, Book, BookForeignObj, BookWeak, BookWithLongName, - BookWithO2O, BookWithoutAuthor, BookWithSlug, IntegerPK, Node, Note, - NoteRename, Tag, TagIndexed, TagM2MTest, TagUniqueRename, Thing, - UniqueTest, new_apps, + Author, AuthorTextFieldWithIndex, AuthorWithDefaultHeight, + AuthorWithEvenLongerName, AuthorWithIndexedName, Book, BookForeignObj, + BookWeak, BookWithLongName, BookWithO2O, BookWithoutAuthor, BookWithSlug, + IntegerPK, Node, Note, NoteRename, Tag, TagIndexed, TagM2MTest, + TagUniqueRename, Thing, UniqueTest, new_apps, ) @@ -1749,6 +1749,13 @@ class SchemaTests(TransactionTestCase): self.get_indexes(Book._meta.db_table), ) + def test_text_field_with_db_index(self): + with connection.schema_editor() as editor: + editor.create_model(AuthorTextFieldWithIndex) + # The text_field index is present if the database supports it. + assertion = self.assertIn if connection.features.supports_index_on_text_field else self.assertNotIn + assertion('text_field', self.get_indexes(AuthorTextFieldWithIndex._meta.db_table)) + def test_primary_key(self): """ Tests altering of the primary key