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_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
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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``
|
* **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
|
||||||
|
|
|
@ -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',
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue