Skip to content

Commit 072ea26

Browse files
authored
Merge pull request #318 from microsoft/add-comments-support
Add db_comment support
2 parents 32d6fb3 + bb2cb08 commit 072ea26

File tree

3 files changed

+134
-15
lines changed

3 files changed

+134
-15
lines changed

mssql/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3333
requires_literal_defaults = True
3434
requires_sqlparse_for_splitting = False
3535
supports_boolean_expr_in_select_clause = False
36+
supports_comments = True
3637
supports_covering_indexes = True
3738
supports_deferrable_unique_constraints = False
3839
supports_expression_indexes = False

mssql/introspection.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from django.db import DatabaseError
55
import pyodbc as Database
66

7+
from collections import namedtuple
8+
79
from django import VERSION
8-
from django.db.backends.base.introspection import (
9-
BaseDatabaseIntrospection, FieldInfo, TableInfo,
10-
)
10+
from django.db.backends.base.introspection import BaseDatabaseIntrospection
11+
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
12+
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
1113
from django.db.models.indexes import Index
1214
from django.conf import settings
1315

@@ -16,6 +18,8 @@
1618
SQL_SMALLAUTOFIELD = -777333
1719
SQL_TIMESTAMP_WITH_TIMEZONE = -155
1820

21+
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("comment",))
22+
TableInfo = namedtuple("TableInfo", BaseTableInfo._fields + ("comment",))
1923

2024
def get_schema_name():
2125
return getattr(settings, 'SCHEMA_TO_INSPECT', 'SCHEMA_NAME()')
@@ -73,13 +77,26 @@ def get_table_list(self, cursor):
7377
"""
7478
Returns a list of table and view names in the current database.
7579
"""
76-
sql = 'SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s' % (
80+
sql = """SELECT
81+
TABLE_NAME,
82+
TABLE_TYPE,
83+
CAST(ep.value AS VARCHAR) AS COMMENT
84+
FROM INFORMATION_SCHEMA.TABLES i
85+
LEFT JOIN sys.tables t ON t.name = i.TABLE_NAME
86+
LEFT JOIN sys.extended_properties ep ON t.object_id = ep.major_id
87+
AND ((ep.name = 'MS_DESCRIPTION' AND ep.minor_id = 0) OR ep.value IS NULL)
88+
AND i.TABLE_SCHEMA = %s""" % (
7789
get_schema_name())
7890
cursor.execute(sql)
7991
types = {'BASE TABLE': 't', 'VIEW': 'v'}
80-
return [TableInfo(row[0], types.get(row[1]))
81-
for row in cursor.fetchall()
82-
if row[0] not in self.ignored_tables]
92+
if VERSION >= (4, 2):
93+
return [TableInfo(row[0], types.get(row[1]), row[2])
94+
for row in cursor.fetchall()
95+
if row[0] not in self.ignored_tables]
96+
else:
97+
return [BaseTableInfo(row[0], types.get(row[1]))
98+
for row in cursor.fetchall()
99+
if row[0] not in self.ignored_tables]
83100

84101
def _is_auto_field(self, cursor, table_name, column_name):
85102
"""
@@ -113,7 +130,7 @@ def get_table_description(self, cursor, table_name, identity_check=True):
113130

114131
if not columns:
115132
raise DatabaseError(f"Table {table_name} does not exist.")
116-
133+
117134
items = []
118135
for column in columns:
119136
if VERSION >= (3, 2):
@@ -128,7 +145,16 @@ def get_table_description(self, cursor, table_name, identity_check=True):
128145
column.append(collation_name[0] if collation_name else '')
129146
else:
130147
column.append('')
131-
148+
if VERSION >= (4, 2):
149+
sql = """select CAST(ep.value AS VARCHAR) AS COMMENT
150+
FROM sys.columns c
151+
INNER JOIN sys.tables t ON c.object_id = t.object_id
152+
INNER JOIN sys.extended_properties ep ON c.object_id=ep.major_id AND ep.minor_id = c.column_id
153+
WHERE t.name = '%s' AND c.name = '%s' AND ep.name = 'MS_Description'
154+
""" % (table_name, column[0])
155+
cursor.execute(sql)
156+
comment = cursor.fetchone()
157+
column.append(comment[0] if comment else '')
132158
if identity_check and self._is_auto_field(cursor, table_name, column[0]):
133159
if column[1] == Database.SQL_BIGINT:
134160
column[1] = SQL_BIGAUTOFIELD
@@ -138,7 +164,10 @@ def get_table_description(self, cursor, table_name, identity_check=True):
138164
column[1] = SQL_AUTOFIELD
139165
if column[1] == Database.SQL_WVARCHAR and column[3] < 4000:
140166
column[1] = Database.SQL_WCHAR
141-
items.append(FieldInfo(*column))
167+
if VERSION >= (4, 2):
168+
items.append(FieldInfo(*column))
169+
else:
170+
items.append(BaseFieldInfo(*column))
142171
return items
143172

144173
def get_sequences(self, cursor, table_name, table_fields=()):

mssql/schema.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,40 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
9393
sql_rename_table = "EXEC sp_rename %(old_table)s, %(new_table)s"
9494
sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \
9595
"WHERE %(columns)s IS NOT NULL"
96-
96+
sql_alter_table_comment= """
97+
IF NOT EXISTS (SELECT NULL FROM sys.extended_properties ep
98+
WHERE ep.major_id = OBJECT_ID('%(table)s')
99+
AND ep.name = 'MS_Description'
100+
AND ep.minor_id = 0)
101+
EXECUTE sp_addextendedproperty
102+
@name = 'MS_Description', @value = %(comment)s,
103+
@level0type = 'SCHEMA', @level0name = 'dbo',
104+
@level1type = 'TABLE', @level1name = %(table)s
105+
ELSE
106+
EXECUTE sp_updateextendedproperty
107+
@name = 'MS_Description', @value = %(comment)s,
108+
@level0type = 'SCHEMA', @level0name = 'dbo',
109+
@level1type = 'TABLE', @level1name = %(table)s
110+
"""
111+
sql_alter_column_comment= """
112+
IF NOT EXISTS (SELECT NULL FROM sys.extended_properties ep
113+
WHERE ep.major_id = OBJECT_ID('%(table)s')
114+
AND ep.name = 'MS_Description'
115+
AND ep.minor_id = (SELECT column_id FROM sys.columns
116+
WHERE name = '%(column)s'
117+
AND object_id = OBJECT_ID('%(table)s')))
118+
EXECUTE sp_addextendedproperty
119+
@name = 'MS_Description', @value = %(comment)s,
120+
@level0type = 'SCHEMA', @level0name = 'dbo',
121+
@level1type = 'TABLE', @level1name = %(table)s,
122+
@level2type = 'COLUMN', @level2name = %(column)s
123+
ELSE
124+
EXECUTE sp_updateextendedproperty
125+
@name = 'MS_Description', @value = %(comment)s,
126+
@level0type = 'SCHEMA', @level0name = 'dbo',
127+
@level1type = 'TABLE', @level1name = %(table)s,
128+
@level2type = 'COLUMN', @level2name = %(column)s
129+
"""
97130
_deferred_unique_indexes = defaultdict(list)
98131

99132
def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
@@ -138,7 +171,18 @@ def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
138171
},
139172
params,
140173
)
141-
174+
175+
def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment):
176+
return (
177+
self.sql_alter_column_comment
178+
% {
179+
"table": self.quote_name(model._meta.db_table),
180+
"column": new_field.column,
181+
"comment": self._comment_sql(new_db_comment),
182+
},
183+
[],
184+
)
185+
142186
def _alter_column_null_sql(self, model, old_field, new_field):
143187
"""
144188
Hook to specialize column null alteration.
@@ -316,7 +360,19 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
316360

317361
# Drop any FK constraints, we'll remake them later
318362
fks_dropped = set()
319-
if old_field.remote_field and old_field.db_constraint:
363+
if (
364+
old_field.remote_field
365+
and old_field.db_constraint
366+
and (django_version < (4,2)
367+
or
368+
(django_version >= (4, 2)
369+
and self._field_should_be_altered(
370+
old_field,
371+
new_field,
372+
ignore={"db_comment"})
373+
)
374+
)
375+
):
320376
# Drop index, SQL Server requires explicit deletion
321377
if not hasattr(new_field, 'db_constraint') or not new_field.db_constraint:
322378
index_names = self._constraint_names(model, [old_field.column], index=True)
@@ -446,8 +502,11 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
446502
actions = []
447503
null_actions = []
448504
post_actions = []
449-
# Type change?
450-
if old_type != new_type:
505+
# Type or comment change?
506+
if old_type != new_type or (django_version >= (4, 2) and
507+
self.connection.features.supports_comments
508+
and old_field.db_comment != new_field.db_comment
509+
):
451510
if django_version >= (4, 2):
452511
fragment, other_actions = self._alter_column_type_sql(
453512
model, old_field, new_field, new_type, old_collation=None, new_collation=None
@@ -922,6 +981,19 @@ def add_field(self, model, field):
922981
"changes": changes_sql,
923982
}
924983
self.execute(sql, params)
984+
# Add field comment, if required.
985+
if django_version >= (4, 2):
986+
if (
987+
field.db_comment
988+
and self.connection.features.supports_comments
989+
and not self.connection.features.supports_comments_inline
990+
):
991+
field_type = db_params["type"]
992+
self.execute(
993+
*self._alter_column_comment_sql(
994+
model, field, field_type, field.db_comment
995+
)
996+
)
925997
# Add an index, if required
926998
self.deferred_sql.extend(self._field_indexes_sql(model, field))
927999
# Add any FK constraints later
@@ -1129,6 +1201,23 @@ def create_model(self, model):
11291201
# Prevent using [] as params, in the case a literal '%' is used in the definition
11301202
self.execute(sql, params or None)
11311203

1204+
if django_version >= (4, 2) and self.connection.features.supports_comments:
1205+
# Add table comment.
1206+
if model._meta.db_table_comment:
1207+
self.alter_db_table_comment(model, None, model._meta.db_table_comment)
1208+
# Add column comments.
1209+
if not self.connection.features.supports_comments_inline:
1210+
for field in model._meta.local_fields:
1211+
if field.db_comment:
1212+
field_db_params = field.db_parameters(
1213+
connection=self.connection
1214+
)
1215+
field_type = field_db_params["type"]
1216+
self.execute(
1217+
*self._alter_column_comment_sql(
1218+
model, field, field_type, field.db_comment
1219+
)
1220+
)
11321221
# Add any field index and index_together's (deferred as SQLite3 _remake_table needs it)
11331222
self.deferred_sql.extend(self._model_indexes_sql(model))
11341223
self.deferred_sql = list(set(self.deferred_sql))

0 commit comments

Comments
 (0)