2019-08-20 15:54:41 +08:00
|
|
|
from django.db import DatabaseError, connection
|
2017-02-16 02:33:55 +08:00
|
|
|
from django.db.models import Index
|
2017-01-20 01:16:04 +08:00
|
|
|
from django.test import TransactionTestCase, skipUnlessDBFeature
|
2009-04-02 12:32:48 +08:00
|
|
|
|
2019-07-27 05:05:22 +08:00
|
|
|
from .models import (
|
2019-08-10 14:41:18 +08:00
|
|
|
Article,
|
|
|
|
ArticleReporter,
|
|
|
|
CheckConstraintModel,
|
|
|
|
City,
|
|
|
|
Comment,
|
|
|
|
Country,
|
2022-10-16 13:59:39 +08:00
|
|
|
DbCommentModel,
|
2021-02-17 17:46:40 +08:00
|
|
|
District,
|
|
|
|
Reporter,
|
|
|
|
UniqueConstraintConditionModel,
|
2019-07-27 05:05:22 +08:00
|
|
|
)
|
2009-04-02 12:32:48 +08:00
|
|
|
|
2012-11-05 02:16:06 +08:00
|
|
|
|
2014-10-19 05:01:13 +08:00
|
|
|
class IntrospectionTests(TransactionTestCase):
|
|
|
|
available_apps = ["introspection"]
|
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_table_names(self):
|
|
|
|
tl = connection.introspection.table_names()
|
2012-04-29 07:11:55 +08:00
|
|
|
self.assertEqual(tl, sorted(tl))
|
2016-04-08 10:04:45 +08:00
|
|
|
self.assertIn(
|
|
|
|
Reporter._meta.db_table,
|
|
|
|
tl,
|
|
|
|
"'%s' isn't in table_list()." % Reporter._meta.db_table,
|
|
|
|
)
|
|
|
|
self.assertIn(
|
|
|
|
Article._meta.db_table,
|
|
|
|
tl,
|
|
|
|
"'%s' isn't in table_list()." % Article._meta.db_table,
|
|
|
|
)
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_django_table_names(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
cursor.execute("CREATE TABLE django_ixn_test_table (id INTEGER);")
|
|
|
|
tl = connection.introspection.django_table_names()
|
|
|
|
cursor.execute("DROP TABLE django_ixn_test_table;")
|
2014-07-10 10:24:19 +08:00
|
|
|
self.assertNotIn(
|
|
|
|
"django_ixn_test_table",
|
|
|
|
tl,
|
|
|
|
"django_table_names() returned a non-Django table",
|
|
|
|
)
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2012-02-12 06:12:49 +08:00
|
|
|
def test_django_table_names_retval_type(self):
|
2015-02-06 02:25:34 +08:00
|
|
|
# Table name is a list #15216
|
2012-02-12 06:12:49 +08:00
|
|
|
tl = connection.introspection.django_table_names(only_existing=True)
|
|
|
|
self.assertIs(type(tl), list)
|
|
|
|
tl = connection.introspection.django_table_names(only_existing=False)
|
|
|
|
self.assertIs(type(tl), list)
|
|
|
|
|
2014-09-21 06:00:52 +08:00
|
|
|
def test_table_names_with_views(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
try:
|
|
|
|
cursor.execute(
|
|
|
|
"CREATE VIEW introspection_article_view AS SELECT headline "
|
|
|
|
"from introspection_article;"
|
|
|
|
)
|
|
|
|
except DatabaseError as e:
|
|
|
|
if "insufficient privileges" in str(e):
|
2014-09-22 01:15:48 +08:00
|
|
|
self.fail("The test user has no CREATE VIEW privileges")
|
2014-09-21 06:00:52 +08:00
|
|
|
else:
|
|
|
|
raise
|
2017-09-27 21:51:49 +08:00
|
|
|
try:
|
|
|
|
self.assertIn(
|
|
|
|
"introspection_article_view",
|
|
|
|
connection.introspection.table_names(include_views=True),
|
|
|
|
)
|
|
|
|
self.assertNotIn(
|
|
|
|
"introspection_article_view", connection.introspection.table_names()
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2017-09-27 21:51:49 +08:00
|
|
|
finally:
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
cursor.execute("DROP VIEW introspection_article_view")
|
2014-09-21 06:00:52 +08:00
|
|
|
|
2016-03-02 12:10:46 +08:00
|
|
|
def test_unmanaged_through_model(self):
|
|
|
|
tables = connection.introspection.django_table_names()
|
|
|
|
self.assertNotIn(ArticleReporter._meta.db_table, tables)
|
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_installed_models(self):
|
|
|
|
tables = [Article._meta.db_table, Reporter._meta.db_table]
|
|
|
|
models = connection.introspection.installed_models(tables)
|
2014-09-26 20:31:50 +08:00
|
|
|
self.assertEqual(models, {Article, Reporter})
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_sequence_list(self):
|
|
|
|
sequences = connection.introspection.sequence_list()
|
2017-09-14 02:12:32 +08:00
|
|
|
reporter_seqs = [
|
|
|
|
seq for seq in sequences if seq["table"] == Reporter._meta.db_table
|
|
|
|
]
|
|
|
|
self.assertEqual(
|
|
|
|
len(reporter_seqs), 1, "Reporter sequence not found in sequence_list()"
|
|
|
|
)
|
|
|
|
self.assertEqual(reporter_seqs[0]["column"], "id")
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_get_table_description_names(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, Reporter._meta.db_table
|
|
|
|
)
|
2009-04-02 12:32:48 +08:00
|
|
|
self.assertEqual(
|
|
|
|
[r[0] for r in desc], [f.column for f in Reporter._meta.fields]
|
|
|
|
)
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_get_table_description_types(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, Reporter._meta.db_table
|
|
|
|
)
|
2010-04-22 07:43:35 +08:00
|
|
|
self.assertEqual(
|
2019-01-19 21:35:51 +08:00
|
|
|
[connection.introspection.get_field_type(r[1], r) for r in desc],
|
2018-10-21 15:08:05 +08:00
|
|
|
[
|
2020-06-02 09:59:03 +08:00
|
|
|
connection.features.introspected_field_types[field]
|
|
|
|
for field in (
|
|
|
|
"AutoField",
|
|
|
|
"CharField",
|
|
|
|
"CharField",
|
|
|
|
"CharField",
|
|
|
|
"BigIntegerField",
|
|
|
|
"BinaryField",
|
|
|
|
"SmallIntegerField",
|
|
|
|
"DurationField",
|
|
|
|
)
|
|
|
|
],
|
2010-04-22 07:43:35 +08:00
|
|
|
)
|
2012-10-27 23:10:02 +08:00
|
|
|
|
|
|
|
def test_get_table_description_col_lengths(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, Reporter._meta.db_table
|
|
|
|
)
|
2012-08-31 01:28:13 +08:00
|
|
|
self.assertEqual(
|
2019-01-19 21:35:51 +08:00
|
|
|
[
|
2022-12-08 16:00:35 +08:00
|
|
|
r[2]
|
2019-01-19 21:35:51 +08:00
|
|
|
for r in desc
|
|
|
|
if connection.introspection.get_field_type(r[1], r) == "CharField"
|
|
|
|
],
|
2014-07-01 02:34:45 +08:00
|
|
|
[30, 30, 254],
|
2012-08-31 01:28:13 +08:00
|
|
|
)
|
2009-04-02 12:32:48 +08:00
|
|
|
|
2012-02-12 04:05:50 +08:00
|
|
|
def test_get_table_description_nullable(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, Reporter._meta.db_table
|
|
|
|
)
|
2014-06-14 05:43:49 +08:00
|
|
|
nullable_by_backend = connection.features.interprets_empty_strings_as_nulls
|
2012-02-12 04:05:50 +08:00
|
|
|
self.assertEqual(
|
|
|
|
[r[6] for r in desc],
|
2018-10-21 15:08:05 +08:00
|
|
|
[
|
|
|
|
False,
|
|
|
|
nullable_by_backend,
|
|
|
|
nullable_by_backend,
|
|
|
|
nullable_by_backend,
|
|
|
|
True,
|
|
|
|
True,
|
|
|
|
False,
|
|
|
|
False,
|
2022-02-04 03:24:19 +08:00
|
|
|
],
|
2012-02-12 04:05:50 +08:00
|
|
|
)
|
|
|
|
|
2015-07-02 16:43:15 +08:00
|
|
|
def test_bigautofield(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, City._meta.db_table
|
|
|
|
)
|
2019-01-19 21:35:51 +08:00
|
|
|
self.assertIn(
|
2020-05-27 06:25:45 +08:00
|
|
|
connection.features.introspected_field_types["BigAutoField"],
|
2019-01-19 21:35:51 +08:00
|
|
|
[connection.introspection.get_field_type(r[1], r) for r in desc],
|
2019-07-27 05:05:22 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
def test_smallautofield(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, Country._meta.db_table
|
|
|
|
)
|
|
|
|
self.assertIn(
|
2020-05-27 06:25:45 +08:00
|
|
|
connection.features.introspected_field_types["SmallAutoField"],
|
2019-07-27 05:05:22 +08:00
|
|
|
[connection.introspection.get_field_type(r[1], r) for r in desc],
|
2019-01-19 21:35:51 +08:00
|
|
|
)
|
2015-07-02 16:43:15 +08:00
|
|
|
|
2022-10-16 13:59:39 +08:00
|
|
|
@skipUnlessDBFeature("supports_comments")
|
|
|
|
def test_db_comments(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, DbCommentModel._meta.db_table
|
|
|
|
)
|
|
|
|
table_list = connection.introspection.get_table_list(cursor)
|
|
|
|
self.assertEqual(
|
|
|
|
["'Name' column comment"],
|
|
|
|
[field.comment for field in desc if field.name == "name"],
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
["Custom table comment"],
|
|
|
|
[
|
|
|
|
table.comment
|
|
|
|
for table in table_list
|
|
|
|
if table.name == "introspection_dbcommentmodel"
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2009-04-04 04:52:54 +08:00
|
|
|
# Regression test for #9991 - 'real' types in postgres
|
2010-10-11 20:55:17 +08:00
|
|
|
@skipUnlessDBFeature("has_real_datatype")
|
|
|
|
def test_postgresql_real_type(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
cursor.execute("CREATE TABLE django_ixn_real_test_table (number REAL);")
|
|
|
|
desc = connection.introspection.get_table_description(
|
|
|
|
cursor, "django_ixn_real_test_table"
|
|
|
|
)
|
|
|
|
cursor.execute("DROP TABLE django_ixn_real_test_table;")
|
2019-01-19 21:35:51 +08:00
|
|
|
self.assertEqual(
|
|
|
|
connection.introspection.get_field_type(desc[0][1], desc[0]), "FloatField"
|
|
|
|
)
|
2009-04-04 04:52:54 +08:00
|
|
|
|
2015-01-10 22:42:31 +08:00
|
|
|
@skipUnlessDBFeature("can_introspect_foreign_keys")
|
2009-04-02 12:32:48 +08:00
|
|
|
def test_get_relations(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
relations = connection.introspection.get_relations(
|
|
|
|
cursor, Article._meta.db_table
|
|
|
|
)
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2015-01-11 03:27:30 +08:00
|
|
|
# That's {field_name: (field_name_other_table, other_table)}
|
|
|
|
expected_relations = {
|
|
|
|
"reporter_id": ("id", Reporter._meta.db_table),
|
|
|
|
"response_to_id": ("id", Article._meta.db_table),
|
|
|
|
}
|
|
|
|
self.assertEqual(relations, expected_relations)
|
|
|
|
|
|
|
|
# Removing a field shouldn't disturb get_relations (#17785)
|
|
|
|
body = Article._meta.get_field("body")
|
|
|
|
with connection.schema_editor() as editor:
|
|
|
|
editor.remove_field(Article, body)
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
relations = connection.introspection.get_relations(
|
|
|
|
cursor, Article._meta.db_table
|
|
|
|
)
|
|
|
|
with connection.schema_editor() as editor:
|
|
|
|
editor.add_field(Article, body)
|
|
|
|
self.assertEqual(relations, expected_relations)
|
2009-04-05 03:03:55 +08:00
|
|
|
|
2011-08-07 08:43:26 +08:00
|
|
|
def test_get_primary_key_column(self):
|
2014-01-09 23:05:15 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
primary_key_column = connection.introspection.get_primary_key_column(
|
|
|
|
cursor, Article._meta.db_table
|
2016-08-20 16:28:42 +08:00
|
|
|
)
|
|
|
|
pk_fk_column = connection.introspection.get_primary_key_column(
|
|
|
|
cursor, District._meta.db_table
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2012-06-08 00:08:47 +08:00
|
|
|
self.assertEqual(primary_key_column, "id")
|
2016-08-20 16:28:42 +08:00
|
|
|
self.assertEqual(pk_fk_column, "city_id")
|
2011-08-07 08:43:26 +08:00
|
|
|
|
2016-08-25 15:12:17 +08:00
|
|
|
def test_get_constraints_index_types(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
constraints = connection.introspection.get_constraints(
|
|
|
|
cursor, Article._meta.db_table
|
|
|
|
)
|
2016-08-25 15:12:17 +08:00
|
|
|
index = {}
|
2017-05-13 00:01:30 +08:00
|
|
|
index2 = {}
|
2017-12-07 06:17:59 +08:00
|
|
|
for val in constraints.values():
|
2016-08-25 15:12:17 +08:00
|
|
|
if val["columns"] == ["headline", "pub_date"]:
|
|
|
|
index = val
|
2017-05-13 00:01:30 +08:00
|
|
|
if val["columns"] == [
|
|
|
|
"headline",
|
|
|
|
"response_to_id",
|
|
|
|
"pub_date",
|
|
|
|
"reporter_id",
|
|
|
|
]:
|
|
|
|
index2 = val
|
2017-02-16 02:33:55 +08:00
|
|
|
self.assertEqual(index["type"], Index.suffix)
|
2017-05-13 00:01:30 +08:00
|
|
|
self.assertEqual(index2["type"], Index.suffix)
|
2016-08-25 15:12:17 +08:00
|
|
|
|
2016-07-26 09:04:28 +08:00
|
|
|
@skipUnlessDBFeature("supports_index_column_ordering")
|
|
|
|
def test_get_constraints_indexes_orders(self):
|
|
|
|
"""
|
|
|
|
Indexes have the 'orders' key with a list of 'ASC'/'DESC' values.
|
|
|
|
"""
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
constraints = connection.introspection.get_constraints(
|
|
|
|
cursor, Article._meta.db_table
|
|
|
|
)
|
|
|
|
indexes_verified = 0
|
|
|
|
expected_columns = [
|
|
|
|
["headline", "pub_date"],
|
2017-05-13 00:01:30 +08:00
|
|
|
["headline", "response_to_id", "pub_date", "reporter_id"],
|
2016-07-26 09:04:28 +08:00
|
|
|
]
|
2020-10-20 12:22:56 +08:00
|
|
|
if connection.features.indexes_foreign_keys:
|
|
|
|
expected_columns += [
|
|
|
|
["reporter_id"],
|
|
|
|
["response_to_id"],
|
|
|
|
]
|
2017-12-07 06:17:59 +08:00
|
|
|
for val in constraints.values():
|
2016-07-26 09:04:28 +08:00
|
|
|
if val["index"] and not (val["primary_key"] or val["unique"]):
|
|
|
|
self.assertIn(val["columns"], expected_columns)
|
|
|
|
self.assertEqual(val["orders"], ["ASC"] * len(val["columns"]))
|
|
|
|
indexes_verified += 1
|
2020-10-20 12:22:56 +08:00
|
|
|
self.assertEqual(indexes_verified, len(expected_columns))
|
2019-02-28 05:47:29 +08:00
|
|
|
|
2021-02-17 17:46:40 +08:00
|
|
|
@skipUnlessDBFeature("supports_index_column_ordering", "supports_partial_indexes")
|
|
|
|
def test_get_constraints_unique_indexes_orders(self):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
constraints = connection.introspection.get_constraints(
|
|
|
|
cursor,
|
|
|
|
UniqueConstraintConditionModel._meta.db_table,
|
|
|
|
)
|
|
|
|
self.assertIn("cond_name_without_color_uniq", constraints)
|
|
|
|
constraint = constraints["cond_name_without_color_uniq"]
|
|
|
|
self.assertIs(constraint["unique"], True)
|
|
|
|
self.assertEqual(constraint["columns"], ["name"])
|
|
|
|
self.assertEqual(constraint["orders"], ["ASC"])
|
|
|
|
|
2019-02-28 05:47:29 +08:00
|
|
|
def test_get_constraints(self):
|
|
|
|
def assertDetails(
|
|
|
|
details,
|
|
|
|
cols,
|
|
|
|
primary_key=False,
|
|
|
|
unique=False,
|
|
|
|
index=False,
|
|
|
|
check=False,
|
|
|
|
foreign_key=None,
|
|
|
|
):
|
|
|
|
# Different backends have different values for same constraints:
|
|
|
|
# PRIMARY KEY UNIQUE CONSTRAINT UNIQUE INDEX
|
|
|
|
# MySQL pk=1 uniq=1 idx=1 pk=0 uniq=1 idx=1 pk=0 uniq=1 idx=1
|
|
|
|
# PostgreSQL pk=1 uniq=1 idx=0 pk=0 uniq=1 idx=0 pk=0 uniq=1 idx=1
|
|
|
|
# SQLite pk=1 uniq=0 idx=0 pk=0 uniq=1 idx=0 pk=0 uniq=1 idx=1
|
|
|
|
if details["primary_key"]:
|
|
|
|
details["unique"] = True
|
|
|
|
if details["unique"]:
|
|
|
|
details["index"] = False
|
|
|
|
self.assertEqual(details["columns"], cols)
|
|
|
|
self.assertEqual(details["primary_key"], primary_key)
|
|
|
|
self.assertEqual(details["unique"], unique)
|
|
|
|
self.assertEqual(details["index"], index)
|
|
|
|
self.assertEqual(details["check"], check)
|
|
|
|
self.assertEqual(details["foreign_key"], foreign_key)
|
|
|
|
|
|
|
|
# Test custom constraints
|
|
|
|
custom_constraints = {
|
|
|
|
"article_email_pub_date_uniq",
|
|
|
|
"email_pub_date_idx",
|
|
|
|
}
|
2019-08-10 14:41:18 +08:00
|
|
|
with connection.cursor() as cursor:
|
|
|
|
constraints = connection.introspection.get_constraints(
|
|
|
|
cursor, Comment._meta.db_table
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
connection.features.supports_column_check_constraints
|
|
|
|
and connection.features.can_introspect_check_constraints
|
|
|
|
):
|
|
|
|
constraints.update(
|
|
|
|
connection.introspection.get_constraints(
|
|
|
|
cursor, CheckConstraintModel._meta.db_table
|
|
|
|
)
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2019-08-10 14:41:18 +08:00
|
|
|
custom_constraints.add("up_votes_gte_0_check")
|
2019-02-28 05:47:29 +08:00
|
|
|
assertDetails(
|
2019-08-10 14:41:18 +08:00
|
|
|
constraints["up_votes_gte_0_check"], ["up_votes"], check=True
|
|
|
|
)
|
|
|
|
assertDetails(
|
2019-02-28 05:47:29 +08:00
|
|
|
constraints["article_email_pub_date_uniq"],
|
|
|
|
["article_id", "email", "pub_date"],
|
|
|
|
unique=True,
|
|
|
|
)
|
|
|
|
assertDetails(
|
|
|
|
constraints["email_pub_date_idx"], ["email", "pub_date"], index=True
|
|
|
|
)
|
|
|
|
# Test field constraints
|
|
|
|
field_constraints = set()
|
|
|
|
for name, details in constraints.items():
|
|
|
|
if name in custom_constraints:
|
|
|
|
continue
|
|
|
|
elif details["columns"] == ["up_votes"] and details["check"]:
|
|
|
|
assertDetails(details, ["up_votes"], check=True)
|
|
|
|
field_constraints.add(name)
|
2019-08-26 15:15:37 +08:00
|
|
|
elif details["columns"] == ["voting_number"] and details["check"]:
|
|
|
|
assertDetails(details, ["voting_number"], check=True)
|
|
|
|
field_constraints.add(name)
|
2019-02-28 05:47:29 +08:00
|
|
|
elif details["columns"] == ["ref"] and details["unique"]:
|
|
|
|
assertDetails(details, ["ref"], unique=True)
|
|
|
|
field_constraints.add(name)
|
2019-08-26 15:15:37 +08:00
|
|
|
elif details["columns"] == ["voting_number"] and details["unique"]:
|
|
|
|
assertDetails(details, ["voting_number"], unique=True)
|
|
|
|
field_constraints.add(name)
|
2019-02-28 05:47:29 +08:00
|
|
|
elif details["columns"] == ["article_id"] and details["index"]:
|
|
|
|
assertDetails(details, ["article_id"], index=True)
|
|
|
|
field_constraints.add(name)
|
|
|
|
elif details["columns"] == ["id"] and details["primary_key"]:
|
|
|
|
assertDetails(details, ["id"], primary_key=True, unique=True)
|
|
|
|
field_constraints.add(name)
|
|
|
|
elif details["columns"] == ["article_id"] and details["foreign_key"]:
|
|
|
|
assertDetails(
|
|
|
|
details, ["article_id"], foreign_key=("introspection_article", "id")
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2019-02-28 05:47:29 +08:00
|
|
|
field_constraints.add(name)
|
|
|
|
elif details["check"]:
|
|
|
|
# Some databases (e.g. Oracle) include additional check
|
|
|
|
# constraints.
|
|
|
|
field_constraints.add(name)
|
|
|
|
# All constraints are accounted for.
|
|
|
|
self.assertEqual(
|
|
|
|
constraints.keys() ^ (custom_constraints | field_constraints), set()
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|