1
0
mirror of https://github.com/django/django.git synced 2025-10-31 01:25:32 +00:00

Fixed #18468 -- Added support for comments on columns and tables.

Thanks Jared Chung, Tom Carrick, David Smith, Nick Pope, and Mariusz
Felisiak for reviews.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
kimsoungryoul
2022-10-16 14:59:39 +09:00
committed by Mariusz Felisiak
parent 68ef274bc5
commit 78f163a4fb
35 changed files with 846 additions and 37 deletions

View File

@@ -78,18 +78,16 @@ class Command(BaseCommand):
)
yield "from %s import models" % self.db_module
known_models = []
table_info = connection.introspection.get_table_list(cursor)
# Determine types of tables and/or views to be introspected.
types = {"t"}
if options["include_partitions"]:
types.add("p")
if options["include_views"]:
types.add("v")
table_info = connection.introspection.get_table_list(cursor)
table_info = {info.name: info for info in table_info if info.type in types}
for table_name in options["table"] or sorted(
info.name for info in table_info if info.type in types
):
for table_name in options["table"] or sorted(name for name in table_info):
if table_name_filter is not None and callable(table_name_filter):
if not table_name_filter(table_name):
continue
@@ -232,6 +230,10 @@ class Command(BaseCommand):
if field_type.startswith(("ForeignKey(", "OneToOneField(")):
field_desc += ", models.DO_NOTHING"
# Add comment.
if connection.features.supports_comments and row.comment:
extra_params["db_comment"] = row.comment
if extra_params:
if not field_desc.endswith("("):
field_desc += ", "
@@ -242,14 +244,22 @@ class Command(BaseCommand):
if comment_notes:
field_desc += " # " + " ".join(comment_notes)
yield " %s" % field_desc
is_view = any(
info.name == table_name and info.type == "v" for info in table_info
)
is_partition = any(
info.name == table_name and info.type == "p" for info in table_info
)
comment = None
if info := table_info.get(table_name):
is_view = info.type == "v"
is_partition = info.type == "p"
if connection.features.supports_comments:
comment = info.comment
else:
is_view = False
is_partition = False
yield from self.get_meta(
table_name, constraints, column_to_field_name, is_view, is_partition
table_name,
constraints,
column_to_field_name,
is_view,
is_partition,
comment,
)
def normalize_col_name(self, col_name, used_column_names, is_relation):
@@ -353,7 +363,13 @@ class Command(BaseCommand):
return field_type, field_params, field_notes
def get_meta(
self, table_name, constraints, column_to_field_name, is_view, is_partition
self,
table_name,
constraints,
column_to_field_name,
is_view,
is_partition,
comment,
):
"""
Return a sequence comprising the lines of code necessary
@@ -391,4 +407,6 @@ class Command(BaseCommand):
if unique_together:
tup = "(" + ", ".join(unique_together) + ",)"
meta += [" unique_together = %s" % tup]
if comment:
meta += [f" db_table_comment = {comment!r}"]
return meta

View File

@@ -334,6 +334,11 @@ class BaseDatabaseFeatures:
# Does the backend support non-deterministic collations?
supports_non_deterministic_collations = True
# Does the backend support column and table comments?
supports_comments = False
# Does the backend support column comments in ADD COLUMN statements?
supports_comments_inline = False
# Does the backend support the logical XOR operator?
supports_logical_xor = False

View File

@@ -141,6 +141,9 @@ class BaseDatabaseSchemaEditor:
sql_delete_procedure = "DROP PROCEDURE %(procedure)s"
sql_alter_table_comment = "COMMENT ON TABLE %(table)s IS %(comment)s"
sql_alter_column_comment = "COMMENT ON COLUMN %(table)s.%(column)s IS %(comment)s"
def __init__(self, connection, collect_sql=False, atomic=True):
self.connection = connection
self.collect_sql = collect_sql
@@ -289,6 +292,8 @@ class BaseDatabaseSchemaEditor:
yield column_db_type
if collation := field_db_params.get("collation"):
yield self._collate_sql(collation)
if self.connection.features.supports_comments_inline and field.db_comment:
yield self._comment_sql(field.db_comment)
# Work out nullability.
null = field.null
# Include a default value, if requested.
@@ -445,6 +450,23 @@ class BaseDatabaseSchemaEditor:
# definition.
self.execute(sql, params or None)
if self.connection.features.supports_comments:
# Add table comment.
if model._meta.db_table_comment:
self.alter_db_table_comment(model, None, model._meta.db_table_comment)
# Add column comments.
if not self.connection.features.supports_comments_inline:
for field in model._meta.local_fields:
if field.db_comment:
field_db_params = field.db_parameters(
connection=self.connection
)
field_type = field_db_params["type"]
self.execute(
*self._alter_column_comment_sql(
model, field, field_type, field.db_comment
)
)
# Add any field index and index_together's (deferred as SQLite
# _remake_table needs it).
self.deferred_sql.extend(self._model_indexes_sql(model))
@@ -614,6 +636,15 @@ class BaseDatabaseSchemaEditor:
if isinstance(sql, Statement):
sql.rename_table_references(old_db_table, new_db_table)
def alter_db_table_comment(self, model, old_db_table_comment, new_db_table_comment):
self.execute(
self.sql_alter_table_comment
% {
"table": self.quote_name(model._meta.db_table),
"comment": self.quote_value(new_db_table_comment or ""),
}
)
def alter_db_tablespace(self, model, old_db_tablespace, new_db_tablespace):
"""Move a model's table between tablespaces."""
self.execute(
@@ -693,6 +724,18 @@ class BaseDatabaseSchemaEditor:
"changes": changes_sql,
}
self.execute(sql, params)
# Add field comment, if required.
if (
field.db_comment
and self.connection.features.supports_comments
and not self.connection.features.supports_comments_inline
):
field_type = db_params["type"]
self.execute(
*self._alter_column_comment_sql(
model, field, field_type, field.db_comment
)
)
# Add an index, if required
self.deferred_sql.extend(self._field_indexes_sql(model, field))
# Reset connection if required
@@ -813,6 +856,11 @@ class BaseDatabaseSchemaEditor:
self.connection.features.supports_foreign_keys
and old_field.remote_field
and old_field.db_constraint
and self._field_should_be_altered(
old_field,
new_field,
ignore={"db_comment"},
)
):
fk_names = self._constraint_names(
model, [old_field.column], foreign_key=True
@@ -949,11 +997,15 @@ class BaseDatabaseSchemaEditor:
# Type suffix change? (e.g. auto increment).
old_type_suffix = old_field.db_type_suffix(connection=self.connection)
new_type_suffix = new_field.db_type_suffix(connection=self.connection)
# Type or collation change?
# Type, collation, or comment change?
if (
old_type != new_type
or old_type_suffix != new_type_suffix
or old_collation != new_collation
or (
self.connection.features.supports_comments
and old_field.db_comment != new_field.db_comment
)
):
fragment, other_actions = self._alter_column_type_sql(
model, old_field, new_field, new_type, old_collation, new_collation
@@ -1211,12 +1263,26 @@ class BaseDatabaseSchemaEditor:
an ALTER TABLE statement and a list of extra (sql, params) tuples to
run once the field is altered.
"""
other_actions = []
if collate_sql := self._collate_sql(
new_collation, old_collation, model._meta.db_table
):
collate_sql = f" {collate_sql}"
else:
collate_sql = ""
# Comment change?
comment_sql = ""
if self.connection.features.supports_comments and not new_field.many_to_many:
if old_field.db_comment != new_field.db_comment:
# PostgreSQL and Oracle can't execute 'ALTER COLUMN ...' and
# 'COMMENT ON ...' at the same time.
sql, params = self._alter_column_comment_sql(
model, new_field, new_type, new_field.db_comment
)
if sql:
other_actions.append((sql, params))
if new_field.db_comment:
comment_sql = self._comment_sql(new_field.db_comment)
return (
(
self.sql_alter_column_type
@@ -1224,12 +1290,27 @@ class BaseDatabaseSchemaEditor:
"column": self.quote_name(new_field.column),
"type": new_type,
"collation": collate_sql,
"comment": comment_sql,
},
[],
),
other_actions,
)
def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment):
return (
self.sql_alter_column_comment
% {
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(new_field.column),
"comment": self._comment_sql(new_db_comment),
},
[],
)
def _comment_sql(self, comment):
return self.quote_value(comment or "")
def _alter_many_to_many(self, model, old_field, new_field, strict):
"""Alter M2Ms to repoint their to= endpoints."""
# Rename the through table
@@ -1423,16 +1504,18 @@ class BaseDatabaseSchemaEditor:
output.append(self._create_index_sql(model, fields=[field]))
return output
def _field_should_be_altered(self, old_field, new_field):
def _field_should_be_altered(self, old_field, new_field, ignore=None):
ignore = ignore or set()
_, old_path, old_args, old_kwargs = old_field.deconstruct()
_, new_path, new_args, new_kwargs = new_field.deconstruct()
# Don't alter when:
# - changing only a field name
# - changing an attribute that doesn't affect the schema
# - changing an attribute in the provided set of ignored attributes
# - adding only a db_column and the column name is not changed
for attr in old_field.non_db_attrs:
for attr in ignore.union(old_field.non_db_attrs):
old_kwargs.pop(attr, None)
for attr in new_field.non_db_attrs:
for attr in ignore.union(new_field.non_db_attrs):
new_kwargs.pop(attr, None)
return self.quote_name(old_field.column) != self.quote_name(
new_field.column

View File

@@ -18,6 +18,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_explicit_null_ordering_when_grouping = True
atomic_transactions = False
can_clone_databases = True
supports_comments = True
supports_comments_inline = True
supports_temporal_subtraction = True
supports_slicing_ordering_in_compound = True
supports_index_on_text_field = False

View File

@@ -5,18 +5,20 @@ from MySQLdb.constants import FIELD_TYPE
from django.db.backends.base.introspection import BaseDatabaseIntrospection
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
from django.db.backends.base.introspection import TableInfo
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
from django.db.models import Index
from django.utils.datastructures import OrderedSet
FieldInfo = namedtuple(
"FieldInfo", BaseFieldInfo._fields + ("extra", "is_unsigned", "has_json_constraint")
"FieldInfo",
BaseFieldInfo._fields + ("extra", "is_unsigned", "has_json_constraint", "comment"),
)
InfoLine = namedtuple(
"InfoLine",
"col_name data_type max_len num_prec num_scale extra column_default "
"collation is_unsigned",
"collation is_unsigned comment",
)
TableInfo = namedtuple("TableInfo", BaseTableInfo._fields + ("comment",))
class DatabaseIntrospection(BaseDatabaseIntrospection):
@@ -68,9 +70,18 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
def get_table_list(self, cursor):
"""Return a list of table and view names in the current database."""
cursor.execute("SHOW FULL TABLES")
cursor.execute(
"""
SELECT
table_name,
table_type,
table_comment
FROM information_schema.tables
WHERE table_schema = DATABASE()
"""
)
return [
TableInfo(row[0], {"BASE TABLE": "t", "VIEW": "v"}.get(row[1]))
TableInfo(row[0], {"BASE TABLE": "t", "VIEW": "v"}.get(row[1]), row[2])
for row in cursor.fetchall()
]
@@ -128,7 +139,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
CASE
WHEN column_type LIKE '%% unsigned' THEN 1
ELSE 0
END AS is_unsigned
END AS is_unsigned,
column_comment
FROM information_schema.columns
WHERE table_name = %s AND table_schema = DATABASE()
""",
@@ -159,6 +171,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
info.extra,
info.is_unsigned,
line[0] in json_constraints,
info.comment,
)
)
return fields

View File

@@ -9,7 +9,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_alter_column_null = "MODIFY %(column)s %(type)s NULL"
sql_alter_column_not_null = "MODIFY %(column)s %(type)s NOT NULL"
sql_alter_column_type = "MODIFY %(column)s %(type)s%(collation)s"
sql_alter_column_type = "MODIFY %(column)s %(type)s%(collation)s%(comment)s"
sql_alter_column_no_default_null = "ALTER COLUMN %(column)s SET DEFAULT NULL"
# No 'CASCADE' which works as a no-op in MySQL but is undocumented
@@ -32,6 +32,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
sql_alter_table_comment = "ALTER TABLE %(table)s COMMENT = %(comment)s"
sql_alter_column_comment = None
@property
def sql_delete_check(self):
if self.connection.mysql_is_mariadb:
@@ -228,3 +231,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def _rename_field_sql(self, table, old_field, new_field, new_type):
new_type = self._set_field_new_type_null_status(old_field, new_type)
return super()._rename_field_sql(table, old_field, new_field, new_type)
def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment):
# Comment is alter when altering the column type.
return "", []
def _comment_sql(self, comment):
comment_sql = super()._comment_sql(comment)
return f" COMMENT {comment_sql}"

View File

@@ -25,6 +25,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_partially_nullable_unique_constraints = False
supports_deferrable_unique_constraints = True
truncates_names = True
supports_comments = True
supports_tablespaces = True
supports_sequence_reset = False
can_introspect_materialized_views = True

View File

@@ -5,10 +5,13 @@ import cx_Oracle
from django.db import models
from django.db.backends.base.introspection import BaseDatabaseIntrospection
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
from django.db.backends.base.introspection import TableInfo
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
from django.utils.functional import cached_property
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("is_autofield", "is_json"))
FieldInfo = namedtuple(
"FieldInfo", BaseFieldInfo._fields + ("is_autofield", "is_json", "comment")
)
TableInfo = namedtuple("TableInfo", BaseTableInfo._fields + ("comment",))
class DatabaseIntrospection(BaseDatabaseIntrospection):
@@ -77,8 +80,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
"""Return a list of table and view names in the current database."""
cursor.execute(
"""
SELECT table_name, 't'
SELECT
user_tables.table_name,
't',
user_tab_comments.comments
FROM user_tables
LEFT OUTER JOIN
user_tab_comments
ON user_tab_comments.table_name = user_tables.table_name
WHERE
NOT EXISTS (
SELECT 1
@@ -86,13 +95,13 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
WHERE user_mviews.mview_name = user_tables.table_name
)
UNION ALL
SELECT view_name, 'v' FROM user_views
SELECT view_name, 'v', NULL FROM user_views
UNION ALL
SELECT mview_name, 'v' FROM user_mviews
SELECT mview_name, 'v', NULL FROM user_mviews
"""
)
return [
TableInfo(self.identifier_converter(row[0]), row[1])
TableInfo(self.identifier_converter(row[0]), row[1], row[2])
for row in cursor.fetchall()
]
@@ -131,10 +140,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
)
THEN 1
ELSE 0
END as is_json
END as is_json,
user_col_comments.comments as col_comment
FROM user_tab_cols
LEFT OUTER JOIN
user_tables ON user_tables.table_name = user_tab_cols.table_name
LEFT OUTER JOIN
user_col_comments ON
user_col_comments.column_name = user_tab_cols.column_name AND
user_col_comments.table_name = user_tab_cols.table_name
WHERE user_tab_cols.table_name = UPPER(%s)
""",
[table_name],
@@ -146,6 +160,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
collation,
is_autofield,
is_json,
comment,
)
for (
column,
@@ -154,6 +169,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
display_size,
is_autofield,
is_json,
comment,
) in cursor.fetchall()
}
self.cache_bust_counter += 1
@@ -165,7 +181,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
description = []
for desc in cursor.description:
name = desc[0]
display_size, default, collation, is_autofield, is_json = field_map[name]
(
display_size,
default,
collation,
is_autofield,
is_json,
comment,
) = field_map[name]
name %= {} # cx_Oracle, for some reason, doubles percent signs.
description.append(
FieldInfo(
@@ -180,6 +203,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
collation,
is_autofield,
is_json,
comment,
)
)
return description

View File

@@ -22,6 +22,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_select_for_update_skip_locked = True
has_select_for_no_key_update = True
can_release_savepoints = True
supports_comments = True
supports_tablespaces = True
supports_transactions = True
can_introspect_materialized_views = True

View File

@@ -2,10 +2,11 @@ from collections import namedtuple
from django.db.backends.base.introspection import BaseDatabaseIntrospection
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
from django.db.backends.base.introspection import TableInfo
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
from django.db.models import Index
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("is_autofield",))
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("is_autofield", "comment"))
TableInfo = namedtuple("TableInfo", BaseTableInfo._fields + ("comment",))
class DatabaseIntrospection(BaseDatabaseIntrospection):
@@ -62,7 +63,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
WHEN c.relispartition THEN 'p'
WHEN c.relkind IN ('m', 'v') THEN 'v'
ELSE 't'
END
END,
obj_description(c.oid)
FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('f', 'm', 'p', 'r', 'v')
@@ -91,7 +93,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable,
pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
CASE WHEN collname = 'default' THEN NULL ELSE collname END AS collation,
a.attidentity != '' AS is_autofield
a.attidentity != '' AS is_autofield,
col_description(a.attrelid, a.attnum) AS column_comment
FROM pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
LEFT JOIN pg_collation co ON a.attcollation = co.oid

View File

@@ -170,6 +170,7 @@ class MigrationAutodetector:
self.generate_created_proxies()
self.generate_altered_options()
self.generate_altered_managers()
self.generate_altered_db_table_comment()
# Create the renamed fields and store them in self.renamed_fields.
# They are used by create_altered_indexes(), generate_altered_fields(),
@@ -1552,6 +1553,28 @@ class MigrationAutodetector:
),
)
def generate_altered_db_table_comment(self):
models_to_check = self.kept_model_keys.union(
self.kept_proxy_keys, self.kept_unmanaged_keys
)
for app_label, model_name in sorted(models_to_check):
old_model_name = self.renamed_models.get(
(app_label, model_name), model_name
)
old_model_state = self.from_state.models[app_label, old_model_name]
new_model_state = self.to_state.models[app_label, model_name]
old_db_table_comment = old_model_state.options.get("db_table_comment")
new_db_table_comment = new_model_state.options.get("db_table_comment")
if old_db_table_comment != new_db_table_comment:
self.add_operation(
app_label,
operations.AlterModelTableComment(
name=model_name,
table_comment=new_db_table_comment,
),
)
def generate_altered_options(self):
"""
Work out if any non-schema-affecting options have changed and make an

View File

@@ -6,6 +6,7 @@ from .models import (
AlterModelManagers,
AlterModelOptions,
AlterModelTable,
AlterModelTableComment,
AlterOrderWithRespectTo,
AlterUniqueTogether,
CreateModel,
@@ -21,6 +22,7 @@ __all__ = [
"CreateModel",
"DeleteModel",
"AlterModelTable",
"AlterModelTableComment",
"AlterUniqueTogether",
"RenameModel",
"AlterIndexTogether",

View File

@@ -529,6 +529,44 @@ class AlterModelTable(ModelOptionOperation):
return "alter_%s_table" % self.name_lower
class AlterModelTableComment(ModelOptionOperation):
def __init__(self, name, table_comment):
self.table_comment = table_comment
super().__init__(name)
def deconstruct(self):
kwargs = {
"name": self.name,
"table_comment": self.table_comment,
}
return (self.__class__.__qualname__, [], kwargs)
def state_forwards(self, app_label, state):
state.alter_model_options(
app_label, self.name_lower, {"db_table_comment": self.table_comment}
)
def database_forwards(self, app_label, schema_editor, from_state, to_state):
new_model = to_state.apps.get_model(app_label, self.name)
if self.allow_migrate_model(schema_editor.connection.alias, new_model):
old_model = from_state.apps.get_model(app_label, self.name)
schema_editor.alter_db_table_comment(
new_model,
old_model._meta.db_table_comment,
new_model._meta.db_table_comment,
)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
return self.database_forwards(app_label, schema_editor, from_state, to_state)
def describe(self):
return f"Alter {self.name} table comment"
@property
def migration_name_fragment(self):
return f"alter_{self.name_lower}_table_comment"
class AlterTogetherOptionOperation(ModelOptionOperation):
option_name = None

View File

@@ -1556,6 +1556,7 @@ class Model(AltersData, metaclass=ModelBase):
*cls._check_ordering(),
*cls._check_constraints(databases),
*cls._check_default_pk(),
*cls._check_db_table_comment(databases),
]
return errors
@@ -1592,6 +1593,29 @@ class Model(AltersData, metaclass=ModelBase):
]
return []
@classmethod
def _check_db_table_comment(cls, databases):
if not cls._meta.db_table_comment:
return []
errors = []
for db in databases:
if not router.allow_migrate_model(db, cls):
continue
connection = connections[db]
if not (
connection.features.supports_comments
or "supports_comments" in cls._meta.required_db_features
):
errors.append(
checks.Warning(
f"{connection.display_name} does not support comments on "
f"tables (db_table_comment).",
obj=cls,
id="models.W046",
)
)
return errors
@classmethod
def _check_swappable(cls):
"""Check if the swapped model exists."""

View File

@@ -200,6 +200,7 @@ class Field(RegisterLookupMixin):
auto_created=False,
validators=(),
error_messages=None,
db_comment=None,
):
self.name = name
self.verbose_name = verbose_name # May be set by set_attributes_from_name
@@ -221,6 +222,7 @@ class Field(RegisterLookupMixin):
self.help_text = help_text
self.db_index = db_index
self.db_column = db_column
self.db_comment = db_comment
self._db_tablespace = db_tablespace
self.auto_created = auto_created
@@ -259,6 +261,7 @@ class Field(RegisterLookupMixin):
*self._check_field_name(),
*self._check_choices(),
*self._check_db_index(),
*self._check_db_comment(**kwargs),
*self._check_null_allowed_for_primary_keys(),
*self._check_backend_specific_checks(**kwargs),
*self._check_validators(),
@@ -385,6 +388,28 @@ class Field(RegisterLookupMixin):
else:
return []
def _check_db_comment(self, databases=None, **kwargs):
if not self.db_comment or not databases:
return []
errors = []
for db in databases:
if not router.allow_migrate_model(db, self.model):
continue
connection = connections[db]
if not (
connection.features.supports_comments
or "supports_comments" in self.model._meta.required_db_features
):
errors.append(
checks.Warning(
f"{connection.display_name} does not support comments on "
f"columns (db_comment).",
obj=self,
id="fields.W163",
)
)
return errors
def _check_null_allowed_for_primary_keys(self):
if (
self.primary_key
@@ -538,6 +563,7 @@ class Field(RegisterLookupMixin):
"choices": None,
"help_text": "",
"db_column": None,
"db_comment": None,
"db_tablespace": None,
"auto_created": False,
"validators": [],

View File

@@ -1428,6 +1428,14 @@ class ManyToManyField(RelatedField):
id="fields.W345",
)
)
if self.db_comment:
warnings.append(
checks.Warning(
"db_comment has no effect on ManyToManyField.",
obj=self,
id="fields.W346",
)
)
return warnings

View File

@@ -30,6 +30,7 @@ DEFAULT_NAMES = (
"verbose_name",
"verbose_name_plural",
"db_table",
"db_table_comment",
"ordering",
"unique_together",
"permissions",
@@ -112,6 +113,7 @@ class Options:
self.verbose_name = None
self.verbose_name_plural = None
self.db_table = ""
self.db_table_comment = ""
self.ordering = []
self._ordering_clash = False
self.indexes = []