1
0
mirror of https://github.com/django/django.git synced 2025-10-31 09:41:08 +00:00

Fixed #30913 -- Added support for covering indexes on PostgreSQL 11+.

This commit is contained in:
Hannes Ljungberg
2019-10-31 13:33:53 +01:00
committed by Mariusz Felisiak
parent f997b5e6ae
commit 8c7992f658
20 changed files with 719 additions and 41 deletions

View File

@@ -277,6 +277,8 @@ class BaseDatabaseFeatures:
# Does the backend support partial indexes (CREATE INDEX ... WHERE ...)?
supports_partial_indexes = True
supports_functions_in_partial_indexes = True
# Does the backend support covering indexes (CREATE INDEX ... INCLUDE ...)?
supports_covering_indexes = False
# Does the database allow more than one constraint or index on the same
# field(s)?

View File

@@ -84,8 +84,8 @@ class BaseDatabaseSchemaEditor:
sql_create_column_inline_fk = None
sql_delete_fk = sql_delete_constraint
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s%(condition)s"
sql_create_unique_index = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)%(condition)s"
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(include)s%(extra)s%(condition)s"
sql_create_unique_index = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)%(include)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)"
@@ -956,9 +956,17 @@ class BaseDatabaseSchemaEditor:
return ' WHERE ' + condition
return ''
def _index_include_sql(self, model, columns):
if not columns or not self.connection.features.supports_covering_indexes:
return ''
return Statement(
' INCLUDE (%(columns)s)',
columns=Columns(model._meta.db_table, columns, self.quote_name),
)
def _create_index_sql(self, model, fields, *, name=None, suffix='', using='',
db_tablespace=None, col_suffixes=(), sql=None, opclasses=(),
condition=None):
condition=None, include=None):
"""
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
@@ -983,6 +991,7 @@ class BaseDatabaseSchemaEditor:
columns=self._index_columns(table, columns, col_suffixes, opclasses),
extra=tablespace_sql,
condition=self._index_condition_sql(condition),
include=self._index_include_sql(model, include),
)
def _delete_index_sql(self, model, name, sql=None):
@@ -1083,16 +1092,22 @@ class BaseDatabaseSchemaEditor:
if deferrable == Deferrable.IMMEDIATE:
return ' DEFERRABLE INITIALLY IMMEDIATE'
def _unique_sql(self, model, fields, name, condition=None, deferrable=None):
def _unique_sql(self, model, fields, name, condition=None, deferrable=None, include=None):
if (
deferrable and
not self.connection.features.supports_deferrable_unique_constraints
):
return None
if condition:
# Databases support conditional unique constraints via a unique
# index.
sql = self._create_unique_sql(model, fields, name=name, condition=condition)
if condition or include:
# Databases support conditional and covering unique constraints via
# a unique index.
sql = self._create_unique_sql(
model,
fields,
name=name,
condition=condition,
include=include,
)
if sql:
self.deferred_sql.append(sql)
return None
@@ -1105,10 +1120,14 @@ class BaseDatabaseSchemaEditor:
'constraint': constraint,
}
def _create_unique_sql(self, model, columns, name=None, condition=None, deferrable=None):
def _create_unique_sql(self, model, columns, name=None, condition=None, deferrable=None, include=None):
if (
deferrable and
not self.connection.features.supports_deferrable_unique_constraints
(
deferrable and
not self.connection.features.supports_deferrable_unique_constraints
) or
(condition and not self.connection.features.supports_partial_indexes) or
(include and not self.connection.features.supports_covering_indexes)
):
return None
@@ -1121,9 +1140,7 @@ class BaseDatabaseSchemaEditor:
else:
name = self.quote_name(name)
columns = Columns(table, columns, self.quote_name)
if condition:
if not self.connection.features.supports_partial_indexes:
return None
if condition or include:
sql = self.sql_create_unique_index
else:
sql = self.sql_create_unique
@@ -1134,20 +1151,24 @@ class BaseDatabaseSchemaEditor:
columns=columns,
condition=self._index_condition_sql(condition),
deferrable=self._deferrable_constraint_sql(deferrable),
include=self._index_include_sql(model, include),
)
def _delete_unique_sql(self, model, name, condition=None, deferrable=None):
def _delete_unique_sql(self, model, name, condition=None, deferrable=None, include=None):
if (
deferrable and
not self.connection.features.supports_deferrable_unique_constraints
(
deferrable and
not self.connection.features.supports_deferrable_unique_constraints
) or
(condition and not self.connection.features.supports_partial_indexes) or
(include and not self.connection.features.supports_covering_indexes)
):
return None
if condition:
return (
self._delete_constraint_sql(self.sql_delete_index, model, name)
if self.connection.features.supports_partial_indexes else None
)
return self._delete_constraint_sql(self.sql_delete_unique, model, name)
if condition or include:
sql = self.sql_delete_index
else:
sql = self.sql_delete_unique
return self._delete_constraint_sql(sql, model, name)
def _check_sql(self, name, check):
return self.sql_constraint % {

View File

@@ -82,3 +82,5 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_brin_autosummarize = property(operator.attrgetter('is_postgresql_10'))
has_websearch_to_tsquery = property(operator.attrgetter('is_postgresql_11'))
supports_table_partitions = property(operator.attrgetter('is_postgresql_10'))
supports_covering_indexes = property(operator.attrgetter('is_postgresql_11'))
supports_covering_gist_indexes = property(operator.attrgetter('is_postgresql_12'))

View File

@@ -12,9 +12,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_set_sequence_max = "SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s"
sql_set_sequence_owner = 'ALTER SEQUENCE %(sequence)s OWNED BY %(table)s.%(column)s'
sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s"
sql_create_index = (
'CREATE INDEX %(name)s ON %(table)s%(using)s '
'(%(columns)s)%(include)s%(extra)s%(condition)s'
)
sql_create_index_concurrently = (
"CREATE INDEX CONCURRENTLY %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s"
'CREATE INDEX CONCURRENTLY %(name)s ON %(table)s%(using)s '
'(%(columns)s)%(include)s%(extra)s%(condition)s'
)
sql_delete_index = "DROP INDEX IF EXISTS %(name)s"
sql_delete_index_concurrently = "DROP INDEX CONCURRENTLY IF EXISTS %(name)s"
@@ -197,10 +201,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def _create_index_sql(
self, model, fields, *, name=None, suffix='', using='',
db_tablespace=None, col_suffixes=(), sql=None, opclasses=(),
condition=None, concurrently=False,
condition=None, concurrently=False, include=None,
):
sql = self.sql_create_index if not concurrently else self.sql_create_index_concurrently
return super()._create_index_sql(
model, fields, name=name, suffix=suffix, using=using, db_tablespace=db_tablespace,
col_suffixes=col_suffixes, sql=sql, opclasses=opclasses, condition=condition,
include=include,
)

View File

@@ -1633,6 +1633,7 @@ class Model(metaclass=ModelBase):
)
)
fields = [field for index in cls._meta.indexes for field, _ in index.fields_orders]
fields += [include for index in cls._meta.indexes for include in index.include]
errors.extend(cls._check_local_fields(fields, 'indexes'))
return errors
@@ -1926,10 +1927,9 @@ class Model(metaclass=ModelBase):
id='models.W038',
)
)
fields = (
field
fields = chain.from_iterable(
(*constraint.fields, *constraint.include)
for constraint in cls._meta.constraints if isinstance(constraint, UniqueConstraint)
for field in constraint.fields
)
errors.extend(cls._check_local_fields(fields, 'constraints'))
return errors

View File

@@ -77,7 +77,7 @@ class Deferrable(Enum):
class UniqueConstraint(BaseConstraint):
def __init__(self, *, fields, name, condition=None, deferrable=None):
def __init__(self, *, fields, name, condition=None, deferrable=None, include=None):
if not fields:
raise ValueError('At least one field is required to define a unique constraint.')
if not isinstance(condition, (type(None), Q)):
@@ -90,9 +90,12 @@ class UniqueConstraint(BaseConstraint):
raise ValueError(
'UniqueConstraint.deferrable must be a Deferrable instance.'
)
if not isinstance(include, (type(None), list, tuple)):
raise ValueError('UniqueConstraint.include must be a list or tuple.')
self.fields = tuple(fields)
self.condition = condition
self.deferrable = deferrable
self.include = tuple(include) if include else ()
super().__init__(name)
def _get_condition_sql(self, model, schema_editor):
@@ -106,31 +109,36 @@ class UniqueConstraint(BaseConstraint):
def constraint_sql(self, model, schema_editor):
fields = [model._meta.get_field(field_name).column for field_name in self.fields]
include = [model._meta.get_field(field_name).column for field_name in self.include]
condition = self._get_condition_sql(model, schema_editor)
return schema_editor._unique_sql(
model, fields, self.name, condition=condition,
deferrable=self.deferrable,
deferrable=self.deferrable, include=include,
)
def create_sql(self, model, schema_editor):
fields = [model._meta.get_field(field_name).column for field_name in self.fields]
include = [model._meta.get_field(field_name).column for field_name in self.include]
condition = self._get_condition_sql(model, schema_editor)
return schema_editor._create_unique_sql(
model, fields, self.name, condition=condition,
deferrable=self.deferrable,
deferrable=self.deferrable, include=include,
)
def remove_sql(self, model, schema_editor):
condition = self._get_condition_sql(model, schema_editor)
include = [model._meta.get_field(field_name).column for field_name in self.include]
return schema_editor._delete_unique_sql(
model, self.name, condition=condition, deferrable=self.deferrable,
include=include,
)
def __repr__(self):
return '<%s: fields=%r name=%r%s%s>' % (
return '<%s: fields=%r name=%r%s%s%s>' % (
self.__class__.__name__, self.fields, self.name,
'' if self.condition is None else ' condition=%s' % self.condition,
'' if self.deferrable is None else ' deferrable=%s' % self.deferrable,
'' if not self.include else ' include=%s' % repr(self.include),
)
def __eq__(self, other):
@@ -139,7 +147,8 @@ class UniqueConstraint(BaseConstraint):
self.name == other.name and
self.fields == other.fields and
self.condition == other.condition and
self.deferrable == other.deferrable
self.deferrable == other.deferrable and
self.include == other.include
)
return super().__eq__(other)
@@ -150,4 +159,6 @@ class UniqueConstraint(BaseConstraint):
kwargs['condition'] = self.condition
if self.deferrable:
kwargs['deferrable'] = self.deferrable
if self.include:
kwargs['include'] = self.include
return path, args, kwargs

View File

@@ -11,7 +11,16 @@ class Index:
# cross-database compatibility with Oracle)
max_name_length = 30
def __init__(self, *, fields=(), name=None, db_tablespace=None, opclasses=(), condition=None):
def __init__(
self,
*,
fields=(),
name=None,
db_tablespace=None,
opclasses=(),
condition=None,
include=None,
):
if opclasses and not name:
raise ValueError('An index must be named to use opclasses.')
if not isinstance(condition, (type(None), Q)):
@@ -26,6 +35,10 @@ class Index:
raise ValueError('Index.fields and Index.opclasses must have the same number of elements.')
if not fields:
raise ValueError('At least one field is required to define an index.')
if include and not name:
raise ValueError('A covering index must be named.')
if not isinstance(include, (type(None), list, tuple)):
raise ValueError('Index.include must be a list or tuple.')
self.fields = list(fields)
# A list of 2-tuple with the field name and ordering ('' or 'DESC').
self.fields_orders = [
@@ -36,6 +49,7 @@ class Index:
self.db_tablespace = db_tablespace
self.opclasses = opclasses
self.condition = condition
self.include = tuple(include) if include else ()
def _get_condition_sql(self, model, schema_editor):
if self.condition is None:
@@ -48,12 +62,13 @@ class Index:
def create_sql(self, model, schema_editor, using='', **kwargs):
fields = [model._meta.get_field(field_name) for field_name, _ in self.fields_orders]
include = [model._meta.get_field(field_name).column for field_name in self.include]
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, condition=condition,
**kwargs,
include=include, **kwargs,
)
def remove_sql(self, model, schema_editor, **kwargs):
@@ -69,6 +84,8 @@ class Index:
kwargs['opclasses'] = self.opclasses
if self.condition:
kwargs['condition'] = self.condition
if self.include:
kwargs['include'] = self.include
return (path, (), kwargs)
def clone(self):
@@ -106,9 +123,10 @@ class Index:
self.name = 'D%s' % self.name[1:]
def __repr__(self):
return "<%s: fields='%s'%s>" % (
return "<%s: fields='%s'%s%s>" % (
self.__class__.__name__, ', '.join(self.fields),
'' if self.condition is None else ', condition=%s' % self.condition,
'' if not self.include else ", include='%s'" % ', '.join(self.include),
)
def __eq__(self, other):