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:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							68ef274bc5
						
					
				
				
					commit
					78f163a4fb
				
			| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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}" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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.""" | ||||
|   | ||||
| @@ -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": [], | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 = [] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user