mirror of
https://github.com/django/django.git
synced 2025-08-03 18:38:50 +00:00
Fixed #30916 -- Added support for functional unique constraints.
Thanks Ian Foote and Mariusz Felisiak for reviews.
This commit is contained in:
parent
19ce1d493a
commit
3aa545281e
15 changed files with 779 additions and 38 deletions
|
@ -2189,6 +2189,246 @@ class SchemaTests(TransactionTestCase):
|
|||
AuthorWithUniqueNameAndBirthday._meta.constraints = []
|
||||
editor.remove_constraint(AuthorWithUniqueNameAndBirthday, constraint)
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(Upper('name').desc(), name='func_upper_uq')
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
sql = constraint.create_sql(Author, editor)
|
||||
table = Author._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
if connection.features.supports_index_column_ordering:
|
||||
self.assertIndexOrder(table, constraint.name, ['DESC'])
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
# SQL contains a database function.
|
||||
self.assertIs(sql.references_column(table, 'name'), True)
|
||||
self.assertIn('UPPER(%s)' % editor.quote_name('name'), str(sql))
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(Author, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_composite_func_unique_constraint(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
editor.create_model(BookWithSlug)
|
||||
constraint = UniqueConstraint(
|
||||
Upper('title'),
|
||||
Lower('slug'),
|
||||
name='func_upper_lower_unq',
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(BookWithSlug, constraint)
|
||||
sql = constraint.create_sql(BookWithSlug, editor)
|
||||
table = BookWithSlug._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
# SQL contains database functions.
|
||||
self.assertIs(sql.references_column(table, 'title'), True)
|
||||
self.assertIs(sql.references_column(table, 'slug'), True)
|
||||
sql = str(sql)
|
||||
self.assertIn('UPPER(%s)' % editor.quote_name('title'), sql)
|
||||
self.assertIn('LOWER(%s)' % editor.quote_name('slug'), sql)
|
||||
self.assertLess(sql.index('UPPER'), sql.index('LOWER'))
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(BookWithSlug, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_unique_constraint_field_and_expression(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(
|
||||
F('height').desc(),
|
||||
'uuid',
|
||||
Lower('name').asc(),
|
||||
name='func_f_lower_field_unq',
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
sql = constraint.create_sql(Author, editor)
|
||||
table = Author._meta.db_table
|
||||
if connection.features.supports_index_column_ordering:
|
||||
self.assertIndexOrder(table, constraint.name, ['DESC', 'ASC', 'ASC'])
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
self.assertEqual(len(constraints[constraint.name]['columns']), 3)
|
||||
self.assertEqual(constraints[constraint.name]['columns'][1], 'uuid')
|
||||
# SQL contains database functions and columns.
|
||||
self.assertIs(sql.references_column(table, 'height'), True)
|
||||
self.assertIs(sql.references_column(table, 'name'), True)
|
||||
self.assertIs(sql.references_column(table, 'uuid'), True)
|
||||
self.assertIn('LOWER(%s)' % editor.quote_name('name'), str(sql))
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(Author, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes', 'supports_partial_indexes')
|
||||
def test_func_unique_constraint_partial(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(
|
||||
Upper('name'),
|
||||
name='func_upper_cond_weight_uq',
|
||||
condition=Q(weight__isnull=False),
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
sql = constraint.create_sql(Author, editor)
|
||||
table = Author._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
self.assertIs(sql.references_column(table, 'name'), True)
|
||||
self.assertIn('UPPER(%s)' % editor.quote_name('name'), str(sql))
|
||||
self.assertIn(
|
||||
'WHERE %s IS NOT NULL' % editor.quote_name('weight'),
|
||||
str(sql),
|
||||
)
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(Author, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes', 'supports_covering_indexes')
|
||||
def test_func_unique_constraint_covering(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(
|
||||
Upper('name'),
|
||||
name='func_upper_covering_uq',
|
||||
include=['weight', 'height'],
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
sql = constraint.create_sql(Author, editor)
|
||||
table = Author._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
self.assertEqual(
|
||||
constraints[constraint.name]['columns'],
|
||||
[None, 'weight', 'height'],
|
||||
)
|
||||
self.assertIs(sql.references_column(table, 'name'), True)
|
||||
self.assertIs(sql.references_column(table, 'weight'), True)
|
||||
self.assertIs(sql.references_column(table, 'height'), True)
|
||||
self.assertIn('UPPER(%s)' % editor.quote_name('name'), str(sql))
|
||||
self.assertIn(
|
||||
'INCLUDE (%s, %s)' % (
|
||||
editor.quote_name('weight'),
|
||||
editor.quote_name('height'),
|
||||
),
|
||||
str(sql),
|
||||
)
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(Author, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint_lookups(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
with register_lookup(CharField, Lower), register_lookup(IntegerField, Abs):
|
||||
constraint = UniqueConstraint(
|
||||
F('name__lower'),
|
||||
F('weight__abs'),
|
||||
name='func_lower_abs_lookup_uq',
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
sql = constraint.create_sql(Author, editor)
|
||||
table = Author._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
# SQL contains columns.
|
||||
self.assertIs(sql.references_column(table, 'name'), True)
|
||||
self.assertIs(sql.references_column(table, 'weight'), True)
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(Author, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint_collate(self):
|
||||
collation = connection.features.test_collations.get('non_default')
|
||||
if not collation:
|
||||
self.skipTest(
|
||||
'This backend does not support case-insensitive collations.'
|
||||
)
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
editor.create_model(BookWithSlug)
|
||||
constraint = UniqueConstraint(
|
||||
Collate(F('title'), collation=collation).desc(),
|
||||
Collate('slug', collation=collation),
|
||||
name='func_collate_uq',
|
||||
)
|
||||
# Add constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(BookWithSlug, constraint)
|
||||
sql = constraint.create_sql(BookWithSlug, editor)
|
||||
table = BookWithSlug._meta.db_table
|
||||
constraints = self.get_constraints(table)
|
||||
self.assertIn(constraint.name, constraints)
|
||||
self.assertIs(constraints[constraint.name]['unique'], True)
|
||||
if connection.features.supports_index_column_ordering:
|
||||
self.assertIndexOrder(table, constraint.name, ['DESC', 'ASC'])
|
||||
# SQL contains columns and a collation.
|
||||
self.assertIs(sql.references_column(table, 'title'), True)
|
||||
self.assertIs(sql.references_column(table, 'slug'), True)
|
||||
self.assertIn('COLLATE %s' % editor.quote_name(collation), str(sql))
|
||||
# Remove constraint.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.remove_constraint(BookWithSlug, constraint)
|
||||
self.assertNotIn(constraint.name, self.get_constraints(table))
|
||||
|
||||
@skipIfDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint_unsupported(self):
|
||||
# UniqueConstraint is ignored on databases that don't support indexes on
|
||||
# expressions.
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(F('name'), name='func_name_uq')
|
||||
with connection.schema_editor() as editor, self.assertNumQueries(0):
|
||||
self.assertIsNone(editor.add_constraint(Author, constraint))
|
||||
self.assertIsNone(editor.remove_constraint(Author, constraint))
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint_nonexistent_field(self):
|
||||
constraint = UniqueConstraint(Lower('nonexistent'), name='func_nonexistent_uq')
|
||||
msg = (
|
||||
"Cannot resolve keyword 'nonexistent' into field. Choices are: "
|
||||
"height, id, name, uuid, weight"
|
||||
)
|
||||
with self.assertRaisesMessage(FieldError, msg):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.add_constraint(Author, constraint)
|
||||
|
||||
@skipUnlessDBFeature('supports_expression_indexes')
|
||||
def test_func_unique_constraint_nondeterministic(self):
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
constraint = UniqueConstraint(Random(), name='func_random_uq')
|
||||
with connection.schema_editor() as editor:
|
||||
with self.assertRaises(DatabaseError):
|
||||
editor.add_constraint(Author, constraint)
|
||||
|
||||
def test_index_together(self):
|
||||
"""
|
||||
Tests removing and adding index_together constraints on a model.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue