Fixed #27859 -- Ignored db_index for TextField/BinaryField on Oracle and MySQL.
Thanks Zubair Alam for the initial patch and Tim Graham for the review.
This commit is contained in:
parent
b3eb6eaf1a
commit
538bf43458
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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**: ``<database>`` does not support a database index on
|
||||
``<field data type>`` columns.
|
||||
* **fields.E900**: ``IPAddressField`` has been removed except for support in
|
||||
historical migrations.
|
||||
* **fields.W900**: ``IPAddressField`` has been deprecated. Support for it
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue