Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -557,6 +557,7 @@ answer newbie questions, and generally made Django that much better:
Kieran Holland <http://www.kieranholland.com>
kilian <kilian.cavalotti@lip6.fr>
Kim Joon Hwan 김준환 <xncbf12@gmail.com>
Kim Soung Ryoul 김성렬 <kimsoungryoul@gmail.com>
Klaas van Schelven <klaas@vanschelven.com>
knox <christobzr@gmail.com>
konrad@gwu.edu
Expand Down
44 changes: 31 additions & 13 deletions django/core/management/commands/inspectdb.py
Expand Up @@ -78,18 +78,16 @@ def table2model(table_name):
)
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
Expand Down Expand Up @@ -232,6 +230,10 @@ def table2model(table_name):
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 += ", "
Expand All @@ -242,14 +244,22 @@ def table2model(table_name):
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):
Expand Down Expand Up @@ -353,7 +363,13 @@ def get_field_type(self, connection, table_name, row):
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
Expand Down Expand Up @@ -391,4 +407,6 @@ def get_meta(
if unique_together:
tup = "(" + ", ".join(unique_together) + ",)"
meta += [" unique_together = %s" % tup]
if comment:
meta += [f" db_table_comment = {comment!r}"]
return meta
5 changes: 5 additions & 0 deletions django/db/backends/base/features.py
Expand Up @@ -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

Expand Down
91 changes: 87 additions & 4 deletions django/db/backends/base/schema.py
Expand Up @@ -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
Expand Down Expand Up @@ -289,6 +292,8 @@ def _iter_column_sql(
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.
Expand Down Expand Up @@ -445,6 +450,23 @@ def create_model(self, model):
# 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))
Expand Down Expand Up @@ -614,6 +636,15 @@ def alter_db_table(self, model, old_db_table, new_db_table):
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(
Expand Down Expand Up @@ -693,6 +724,18 @@ def add_field(self, model, field):
"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
Expand Down Expand Up @@ -813,6 +856,11 @@ def _alter_field(
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
Expand Down Expand Up @@ -949,11 +997,15 @@ def _alter_field(
# 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
Expand Down Expand Up @@ -1211,25 +1263,54 @@ def _alter_column_type_sql(
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
% {
"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
Expand Down Expand Up @@ -1423,16 +1504,18 @@ def _field_indexes_sql(self, model, field):
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
Expand Down
2 changes: 2 additions & 0 deletions django/db/backends/mysql/features.py
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions django/db/backends/mysql/introspection.py
Expand Up @@ -5,18 +5,20 @@

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):
Expand Down Expand Up @@ -68,9 +70,18 @@ def get_field_type(self, data_type, description):

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()
]

Expand Down Expand Up @@ -128,7 +139,8 @@ def get_table_description(self, cursor, table_name):
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()
""",
Expand Down Expand Up @@ -159,6 +171,7 @@ def to_int(i):
info.extra,
info.is_unsigned,
line[0] in json_constraints,
info.comment,
)
)
return fields
Expand Down
13 changes: 12 additions & 1 deletion django/db/backends/mysql/schema.py
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -228,3 +231,11 @@ def _alter_column_type_sql(
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}"
1 change: 1 addition & 0 deletions django/db/backends/oracle/features.py
Expand Up @@ -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
Expand Down