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:
Mariusz Felisiak 2017-05-23 17:02:40 +02:00 committed by GitHub
parent b3eb6eaf1a
commit 538bf43458
13 changed files with 114 additions and 19 deletions

View File

@ -227,6 +227,9 @@ class BaseDatabaseFeatures:
supports_select_difference = True supports_select_difference = True
supports_slicing_ordering_in_compound = False supports_slicing_ordering_in_compound = False
# Does the backend support indexing a TextField?
supports_index_on_text_field = True
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection

View File

@ -143,6 +143,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
else: else:
return self._data_types 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 = { operators = {
'exact': '= %s', 'exact': '= %s',
'iexact': 'LIKE %s', 'iexact': 'LIKE %s',

View File

@ -31,6 +31,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_select_intersection = False supports_select_intersection = False
supports_select_difference = False supports_select_difference = False
supports_slicing_ordering_in_compound = True supports_slicing_ordering_in_compound = True
supports_index_on_text_field = False
@cached_property @cached_property
def _mysql_storage_engine(self): def _mysql_storage_engine(self):

View File

@ -29,20 +29,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
import MySQLdb.converters import MySQLdb.converters
return MySQLdb.escape(value, MySQLdb.converters.conversions) return MySQLdb.escape(value, MySQLdb.converters.conversions)
def skip_default(self, field): def _is_limited_data_type(self, field):
"""
MySQL doesn't accept default values for some data types and implicitly
treats these columns as nullable.
"""
db_type = field.db_type(self.connection) db_type = field.db_type(self.connection)
return ( return db_type is not None and db_type.lower() in self.connection._limited_data_types
db_type is not None and
db_type.lower() in { def skip_default(self, field):
'tinyblob', 'blob', 'mediumblob', 'longblob', return self._is_limited_data_type(field)
'tinytext', 'text', 'mediumtext', 'longtext',
'json',
}
)
def add_field(self, model, field): def add_field(self, model, field):
super().add_field(model, field) super().add_field(model, field)
@ -69,6 +61,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
field.get_internal_type() == 'ForeignKey' and field.get_internal_type() == 'ForeignKey' and
field.db_constraint): field.db_constraint):
return False return False
if self._is_limited_data_type(field):
return False
return create_index return create_index
def _delete_composed_index(self, model, fields, *args): def _delete_composed_index(self, model, fields, *args):

View File

@ -31,6 +31,7 @@ class DatabaseValidation(BaseDatabaseValidation):
MySQL has the following field length restriction: MySQL has the following field length restriction:
No character (varchar) fields can have a length exceeding 255 No character (varchar) fields can have a length exceeding 255
characters if they have a unique index on them. characters if they have a unique index on them.
MySQL doesn't support a database index on some data types.
""" """
errors = [] errors = []
if (field_type.startswith('varchar') and field.unique and if (field_type.startswith('varchar') and field.unique and
@ -42,4 +43,18 @@ class DatabaseValidation(BaseDatabaseValidation):
id='mysql.E001', 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 return errors

View File

@ -55,6 +55,7 @@ from .introspection import DatabaseIntrospection # NOQA isort:skip
from .operations import DatabaseOperations # NOQA isort:skip from .operations import DatabaseOperations # NOQA isort:skip
from .schema import DatabaseSchemaEditor # NOQA isort:skip from .schema import DatabaseSchemaEditor # NOQA isort:skip
from .utils import Oracle_datetime # NOQA isort:skip from .utils import Oracle_datetime # NOQA isort:skip
from .validation import DatabaseValidation # NOQA isort:skip
class _UninitializedOperatorsDescriptor: class _UninitializedOperatorsDescriptor:
@ -115,6 +116,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'PositiveSmallIntegerField': '%(qn_column)s >= 0', 'PositiveSmallIntegerField': '%(qn_column)s >= 0',
} }
# Oracle doesn't support a database index on these columns.
_limited_data_types = ('clob', 'nclob', 'blob')
operators = _UninitializedOperatorsDescriptor() operators = _UninitializedOperatorsDescriptor()
_standard_operators = { _standard_operators = {
@ -174,6 +178,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
features_class = DatabaseFeatures features_class = DatabaseFeatures
introspection_class = DatabaseIntrospection introspection_class = DatabaseIntrospection
ops_class = DatabaseOperations ops_class = DatabaseOperations
validation_class = DatabaseValidation
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -35,3 +35,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
# Oracle doesn't ignore quoted identifiers case but the current backend # Oracle doesn't ignore quoted identifiers case but the current backend
# does by uppercasing all identifiers. # does by uppercasing all identifiers.
ignores_table_name_case = True ignores_table_name_case = True
supports_index_on_text_field = False

View File

@ -119,3 +119,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def prepare_default(self, value): def prepare_default(self, value):
return self.quote_value(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

View File

@ -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

View File

@ -172,6 +172,8 @@ Model fields
* **fields.E160**: The options ``auto_now``, ``auto_now_add``, and ``default`` * **fields.E160**: The options ``auto_now``, ``auto_now_add``, and ``default``
are mutually exclusive. Only one of these options may be present. are mutually exclusive. Only one of these options may be present.
* **fields.W161**: Fixed default value provided. * **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 * **fields.E900**: ``IPAddressField`` has been removed except for support in
historical migrations. historical migrations.
* **fields.W900**: ``IPAddressField`` has been deprecated. Support for it * **fields.W900**: ``IPAddressField`` has been deprecated. Support for it

View File

@ -2,7 +2,7 @@ import unittest
from django.core.checks import Error, Warning as DjangoWarning from django.core.checks import Error, Warning as DjangoWarning
from django.db import connection, models 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.test.utils import isolate_apps, override_settings
from django.utils.timezone import now from django.utils.timezone import now
@ -646,3 +646,26 @@ class TimeFieldTests(TestCase):
@override_settings(USE_TZ=True) @override_settings(USE_TZ=True)
def test_fix_default_value_tz(self): def test_fix_default_value_tz(self):
self.test_fix_default_value() 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',
)
])

View File

@ -17,6 +17,13 @@ class Author(models.Model):
apps = new_apps apps = new_apps
class AuthorTextFieldWithIndex(models.Model):
text_field = models.TextField(db_index=True)
class Meta:
apps = new_apps
class AuthorWithDefaultHeight(models.Model): class AuthorWithDefaultHeight(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
height = models.PositiveIntegerField(null=True, blank=True, default=42) height = models.PositiveIntegerField(null=True, blank=True, default=42)

View File

@ -29,11 +29,11 @@ from .fields import (
CustomManyToManyField, InheritedManyToManyField, MediumBlobField, CustomManyToManyField, InheritedManyToManyField, MediumBlobField,
) )
from .models import ( from .models import (
Author, AuthorWithDefaultHeight, AuthorWithEvenLongerName, Author, AuthorTextFieldWithIndex, AuthorWithDefaultHeight,
AuthorWithIndexedName, Book, BookForeignObj, BookWeak, BookWithLongName, AuthorWithEvenLongerName, AuthorWithIndexedName, Book, BookForeignObj,
BookWithO2O, BookWithoutAuthor, BookWithSlug, IntegerPK, Node, Note, BookWeak, BookWithLongName, BookWithO2O, BookWithoutAuthor, BookWithSlug,
NoteRename, Tag, TagIndexed, TagM2MTest, TagUniqueRename, Thing, IntegerPK, Node, Note, NoteRename, Tag, TagIndexed, TagM2MTest,
UniqueTest, new_apps, TagUniqueRename, Thing, UniqueTest, new_apps,
) )
@ -1749,6 +1749,13 @@ class SchemaTests(TransactionTestCase):
self.get_indexes(Book._meta.db_table), 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): def test_primary_key(self):
""" """
Tests altering of the primary key Tests altering of the primary key