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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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',
)
])

View File

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

View File

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