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:
committed by
Mariusz Felisiak
parent
f997b5e6ae
commit
8c7992f658
@@ -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)?
|
||||
|
||||
@@ -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 % {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user