mirror of
https://github.com/django/django.git
synced 2025-10-23 21:59:11 +00:00
Fixed #29547 -- Added support for partial indexes.
Thanks to Ian Foote, Mariusz Felisiak, Simon Charettes, and Markus Holtermann for comments and feedback.
This commit is contained in:
@@ -38,6 +38,7 @@ class PostGISSchemaEditor(DatabaseSchemaEditor):
|
||||
"using": "USING %s" % self.geom_index_type,
|
||||
"columns": field_column,
|
||||
"extra": '',
|
||||
"condition": '',
|
||||
}
|
||||
|
||||
def _alter_column_type_sql(self, table, old_field, new_field, new_type):
|
||||
|
||||
@@ -276,6 +276,9 @@ class BaseDatabaseFeatures:
|
||||
# in UPDATE statements to ensure the expression has the correct type?
|
||||
requires_casted_case_in_updates = False
|
||||
|
||||
# Does the backend support partial indexes (CREATE INDEX ... WHERE ...)?
|
||||
supports_partial_indexes = True
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class BaseDatabaseSchemaEditor:
|
||||
sql_create_inline_fk = None
|
||||
sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"
|
||||
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s%(condition)s"
|
||||
sql_delete_index = "DROP INDEX %(name)s"
|
||||
|
||||
sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s PRIMARY KEY (%(columns)s)"
|
||||
@@ -326,7 +326,7 @@ class BaseDatabaseSchemaEditor:
|
||||
|
||||
def add_index(self, model, index):
|
||||
"""Add an index on a model."""
|
||||
self.execute(index.create_sql(model, self))
|
||||
self.execute(index.create_sql(model, self), params=None)
|
||||
|
||||
def remove_index(self, model, index):
|
||||
"""Remove an index from a model."""
|
||||
@@ -905,7 +905,8 @@ class BaseDatabaseSchemaEditor:
|
||||
return ''
|
||||
|
||||
def _create_index_sql(self, model, fields, *, name=None, suffix='', using='',
|
||||
db_tablespace=None, col_suffixes=(), sql=None, opclasses=()):
|
||||
db_tablespace=None, col_suffixes=(), sql=None, opclasses=(),
|
||||
condition=''):
|
||||
"""
|
||||
Return the SQL statement to create the index for one or several fields.
|
||||
`sql` can be specified if the syntax differs from the standard (GIS
|
||||
@@ -929,6 +930,7 @@ class BaseDatabaseSchemaEditor:
|
||||
using=using,
|
||||
columns=self._index_columns(table, columns, col_suffixes, opclasses),
|
||||
extra=tablespace_sql,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
def _index_columns(self, table, columns, col_suffixes, opclasses):
|
||||
|
||||
@@ -51,6 +51,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
db_functions_convert_bytes_to_str = True
|
||||
# Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
|
||||
supported_explain_formats = {'JSON', 'TEXT', 'TRADITIONAL'}
|
||||
# Neither MySQL nor MariaDB support partial indexes.
|
||||
supports_partial_indexes = False
|
||||
|
||||
@cached_property
|
||||
def _mysql_storage_engine(self):
|
||||
|
||||
@@ -24,6 +24,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||
sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s PRIMARY KEY (%(columns)s)"
|
||||
sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY"
|
||||
|
||||
sql_create_index = 'CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s'
|
||||
|
||||
def quote_value(self, value):
|
||||
self.connection.ensure_connection()
|
||||
quoted = self.connection.connection.escape(value, self.connection.connection.encoders)
|
||||
|
||||
@@ -55,6 +55,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
supports_over_clause = True
|
||||
supports_ignore_conflicts = False
|
||||
max_query_params = 2**16 - 1
|
||||
supports_partial_indexes = False
|
||||
|
||||
@cached_property
|
||||
def has_fetch_offset_support(self):
|
||||
|
||||
@@ -16,6 +16,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||
sql_alter_column_no_default = "MODIFY %(column)s DEFAULT NULL"
|
||||
sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
|
||||
sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, (datetime.date, datetime.time, datetime.datetime)):
|
||||
|
||||
@@ -12,7 +12,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||
sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE"
|
||||
sql_set_sequence_max = "SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s"
|
||||
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s"
|
||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s"
|
||||
sql_delete_index = "DROP INDEX IF EXISTS %(name)s"
|
||||
|
||||
# Setting the constraint to IMMEDIATE runs any deferred checks to allow
|
||||
|
||||
@@ -34,6 +34,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
supports_cast_with_precision = False
|
||||
time_cast_precision = 3
|
||||
can_release_savepoints = True
|
||||
supports_partial_indexes = Database.version_info >= (3, 8, 0)
|
||||
# Is "ALTER TABLE ... RENAME COLUMN" supported?
|
||||
can_alter_table_rename_column = Database.sqlite_version_info >= (3, 25, 0)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.db.backends.utils import names_digest, split_identifier
|
||||
from django.db.models.query_utils import Q
|
||||
from django.db.models.sql import Query
|
||||
|
||||
__all__ = ['Index']
|
||||
|
||||
@@ -9,9 +11,13 @@ class Index:
|
||||
# cross-database compatibility with Oracle)
|
||||
max_name_length = 30
|
||||
|
||||
def __init__(self, *, fields=(), name=None, db_tablespace=None, opclasses=()):
|
||||
def __init__(self, *, fields=(), name=None, db_tablespace=None, opclasses=(), condition=None):
|
||||
if opclasses and not name:
|
||||
raise ValueError('An index must be named to use opclasses.')
|
||||
if not isinstance(condition, (type(None), Q)):
|
||||
raise ValueError('Index.condition must be a Q instance.')
|
||||
if condition and not name:
|
||||
raise ValueError('An index must be named to use condition.')
|
||||
if not isinstance(fields, (list, tuple)):
|
||||
raise ValueError('Index.fields must be a list or tuple.')
|
||||
if not isinstance(opclasses, (list, tuple)):
|
||||
@@ -35,6 +41,7 @@ class Index:
|
||||
raise ValueError(errors)
|
||||
self.db_tablespace = db_tablespace
|
||||
self.opclasses = opclasses
|
||||
self.condition = condition
|
||||
|
||||
def check_name(self):
|
||||
errors = []
|
||||
@@ -48,12 +55,25 @@ class Index:
|
||||
self.name = 'D%s' % self.name[1:]
|
||||
return errors
|
||||
|
||||
def _get_condition_sql(self, model, schema_editor):
|
||||
if self.condition is None:
|
||||
return ''
|
||||
query = Query(model=model)
|
||||
query.add_q(self.condition)
|
||||
compiler = query.get_compiler(connection=schema_editor.connection)
|
||||
# Only the WhereNode is of interest for the partial index.
|
||||
sql, params = query.where.as_sql(compiler=compiler, connection=schema_editor.connection)
|
||||
# BaseDatabaseSchemaEditor does the same map on the params, but since
|
||||
# it's handled outside of that class, the work is done here.
|
||||
return ' WHERE ' + (sql % tuple(map(schema_editor.quote_value, params)))
|
||||
|
||||
def create_sql(self, model, schema_editor, using=''):
|
||||
fields = [model._meta.get_field(field_name) for field_name, _ in self.fields_orders]
|
||||
col_suffixes = [order[1] for order in self.fields_orders]
|
||||
condition = self._get_condition_sql(model, schema_editor)
|
||||
return schema_editor._create_index_sql(
|
||||
model, fields, name=self.name, using=using, db_tablespace=self.db_tablespace,
|
||||
col_suffixes=col_suffixes, opclasses=self.opclasses,
|
||||
col_suffixes=col_suffixes, opclasses=self.opclasses, condition=condition,
|
||||
)
|
||||
|
||||
def remove_sql(self, model, schema_editor):
|
||||
@@ -71,6 +91,8 @@ class Index:
|
||||
kwargs['db_tablespace'] = self.db_tablespace
|
||||
if self.opclasses:
|
||||
kwargs['opclasses'] = self.opclasses
|
||||
if self.condition:
|
||||
kwargs['condition'] = self.condition
|
||||
return (path, (), kwargs)
|
||||
|
||||
def clone(self):
|
||||
@@ -107,7 +129,10 @@ class Index:
|
||||
self.check_name()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: fields='%s'>" % (self.__class__.__name__, ', '.join(self.fields))
|
||||
return "<%s: fields='%s'%s>" % (
|
||||
self.__class__.__name__, ', '.join(self.fields),
|
||||
'' if self.condition is None else ', condition=%s' % self.condition,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__) and (self.deconstruct() == other.deconstruct())
|
||||
|
||||
Reference in New Issue
Block a user