summaryrefslogtreecommitdiff
path: root/tests/schema
diff options
context:
space:
mode:
authorHannes Ljungberg <hannes.ljungberg@gmail.com>2021-02-06 20:45:54 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-02-23 20:19:53 +0100
commit3aa545281e0c0f9fac93753e3769df9e0334dbaa (patch)
tree454bec922a78df9ecfe6eab197e7750e8a5bafac /tests/schema
parent19ce1d493ae7cbc1e704d338077b1f5f5e5769c9 (diff)
Fixed #30916 -- Added support for functional unique constraints.
Thanks Ian Foote and Mariusz Felisiak for reviews.
Diffstat (limited to 'tests/schema')
-rw-r--r--tests/schema/tests.py240
1 files changed, 240 insertions, 0 deletions
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 4522f5faf4..fd8874e157 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -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.