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

Add db_comment support #318

Merged
merged 40 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0a51811
Allow Django 4.2 (#227)
maikhanhbui Feb 16, 2023
81018de
Fix errors with raising FullResultSet exception and with alter_column…
maikhanhbui Feb 21, 2023
06e0788
fix last_executed_query() to properly replace placeholders with param…
maikhanhbui Mar 3, 2023
6875151
disable allows_group_by_select_index
mShan0 Mar 10, 2023
9ac7c2f
unskip old tests
mShan0 Mar 10, 2023
9816cdf
unskip some tests
mShan0 Mar 13, 2023
8e5c9cd
skip more tests
mShan0 Mar 13, 2023
895909c
Merge pull request #236 from microsoft/fix-aggregate-tests
mShan0 Mar 13, 2023
127838e
Merge branch 'dev' of github.com:microsoft/mssql-django into django42
maikhanhbui Mar 13, 2023
303859c
Use latest Django 4.2 beta for tox (#238)
mShan0 Mar 15, 2023
e196f5a
use 4.2 rc1 branch (#240)
mShan0 Mar 20, 2023
5a630ba
allow partial support for filtering against window functions (#239)
maikhanhbui Mar 23, 2023
adc1409
add subsecond support to Now() (#242)
maikhanhbui Mar 24, 2023
0c4d052
assign value to display_size (#244)
maikhanhbui Apr 4, 2023
f9a1b43
add latest django 4.2 branch to ci
mShan0 Apr 4, 2023
b311485
add latest django 4.2 branch to ci
mShan0 Apr 5, 2023
c7445a8
allow comments on columns and tables
maikhanhbui Apr 17, 2023
4a37e32
raise an error when batch_size is zero. (#259)
maikhanhbui Apr 24, 2023
e145d85
Merge branch 'django42' into add-comments-support
maikhanhbui Apr 29, 2023
3375f3a
replicate get or create test for mssql (#265)
mShan0 May 3, 2023
a3e7f72
Merge branch 'django42' into add-comments-support
mShan0 May 23, 2023
068c7fc
add table comment to `get_table_list` query
mShan0 May 23, 2023
e938360
add column comment to `get_table_description()`
mShan0 May 23, 2023
852d67d
return column comment only for `get_table_description()`
maikhanhbui May 25, 2023
0a2664a
Add skipped tests to Django 4.2 (#268)
maikhanhbui May 30, 2023
f2d82e9
Merge branch 'dev' into django42
mShan0 May 30, 2023
a3e895b
syntax fix
mShan0 May 31, 2023
19e1539
ci fix
mShan0 May 31, 2023
2885672
bump version to 1.3
mShan0 May 31, 2023
08aa6f5
Merge branch 'django42' into add-comments-support
maikhanhbui Jul 6, 2023
716a27a
Merge branch 'dev' into add-comments-support
maikhanhbui Jul 21, 2023
9c3be2b
add partial support for adding/altering comments on columns and tables
maikhanhbui Jul 25, 2023
be36161
fix sql
maikhanhbui Jul 27, 2023
8f9f00a
add django 4.2 condition and fix sql
maikhanhbui Jul 27, 2023
f9190be
fix sql
maikhanhbui Jul 27, 2023
2590023
Implement db_comment
dauinsight Dec 8, 2023
69d3024
Merge branch 'dev' into add-comments-support
dauinsight Dec 8, 2023
965573a
Update get_table_list and add version condition to db_comment
dauinsight Dec 8, 2023
37d48d0
Fix drop fk condition
dauinsight Dec 11, 2023
bb2cb08
Fix alter comment behavior
dauinsight Dec 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mssql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_literal_defaults = True
requires_sqlparse_for_splitting = False
supports_boolean_expr_in_select_clause = False
supports_comments = True
supports_covering_indexes = True
supports_deferrable_unique_constraints = False
supports_expression_indexes = False
Expand Down
49 changes: 39 additions & 10 deletions mssql/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from django.db import DatabaseError
import pyodbc as Database

from collections import namedtuple

from django import VERSION
from django.db.backends.base.introspection import (
BaseDatabaseIntrospection, FieldInfo, TableInfo,
)
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 as BaseTableInfo
from django.db.models.indexes import Index
from django.conf import settings

Expand All @@ -16,6 +18,8 @@
SQL_SMALLAUTOFIELD = -777333
SQL_TIMESTAMP_WITH_TIMEZONE = -155

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

def get_schema_name():
return getattr(settings, 'SCHEMA_TO_INSPECT', 'SCHEMA_NAME()')
Expand Down Expand Up @@ -73,13 +77,26 @@ def get_table_list(self, cursor):
"""
Returns a list of table and view names in the current database.
"""
sql = 'SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s' % (
sql = """SELECT
TABLE_NAME,
TABLE_TYPE,
CAST(ep.value AS VARCHAR) AS COMMENT
FROM INFORMATION_SCHEMA.TABLES i
LEFT JOIN sys.tables t ON t.name = i.TABLE_NAME
LEFT JOIN sys.extended_properties ep ON t.object_id = ep.major_id
AND ((ep.name = 'MS_DESCRIPTION' AND ep.minor_id = 0) OR ep.value IS NULL)
AND i.TABLE_SCHEMA = %s""" % (
get_schema_name())
cursor.execute(sql)
types = {'BASE TABLE': 't', 'VIEW': 'v'}
return [TableInfo(row[0], types.get(row[1]))
for row in cursor.fetchall()
if row[0] not in self.ignored_tables]
if VERSION >= (4, 2):
return [TableInfo(row[0], types.get(row[1]), row[2])
for row in cursor.fetchall()
if row[0] not in self.ignored_tables]
else:
return [BaseTableInfo(row[0], types.get(row[1]))
for row in cursor.fetchall()
if row[0] not in self.ignored_tables]

def _is_auto_field(self, cursor, table_name, column_name):
"""
Expand Down Expand Up @@ -113,7 +130,7 @@ def get_table_description(self, cursor, table_name, identity_check=True):

if not columns:
raise DatabaseError(f"Table {table_name} does not exist.")

items = []
for column in columns:
if VERSION >= (3, 2):
Expand All @@ -128,7 +145,16 @@ def get_table_description(self, cursor, table_name, identity_check=True):
column.append(collation_name[0] if collation_name else '')
else:
column.append('')

if VERSION >= (4, 2):
sql = """select CAST(ep.value AS VARCHAR) AS COMMENT
FROM sys.columns c
INNER JOIN sys.tables t ON c.object_id = t.object_id
INNER JOIN sys.extended_properties ep ON c.object_id=ep.major_id AND ep.minor_id = c.column_id
WHERE t.name = '%s' AND c.name = '%s' AND ep.name = 'MS_Description'
""" % (table_name, column[0])
cursor.execute(sql)
comment = cursor.fetchone()
column.append(comment[0] if comment else '')
if identity_check and self._is_auto_field(cursor, table_name, column[0]):
if column[1] == Database.SQL_BIGINT:
column[1] = SQL_BIGAUTOFIELD
Expand All @@ -138,7 +164,10 @@ def get_table_description(self, cursor, table_name, identity_check=True):
column[1] = SQL_AUTOFIELD
if column[1] == Database.SQL_WVARCHAR and column[3] < 4000:
column[1] = Database.SQL_WCHAR
items.append(FieldInfo(*column))
if VERSION >= (4, 2):
items.append(FieldInfo(*column))
else:
items.append(BaseFieldInfo(*column))
return items

def get_sequences(self, cursor, table_name, table_fields=()):
Expand Down
99 changes: 94 additions & 5 deletions mssql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,40 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_rename_table = "EXEC sp_rename %(old_table)s, %(new_table)s"
sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \
"WHERE %(columns)s IS NOT NULL"

sql_alter_table_comment= """
IF NOT EXISTS (SELECT NULL FROM sys.extended_properties ep
WHERE ep.major_id = OBJECT_ID('%(table)s')
AND ep.name = 'MS_Description'
AND ep.minor_id = 0)
EXECUTE sp_addextendedproperty
@name = 'MS_Description', @value = %(comment)s,
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = %(table)s
ELSE
EXECUTE sp_updateextendedproperty
@name = 'MS_Description', @value = %(comment)s,
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = %(table)s
"""
sql_alter_column_comment= """
IF NOT EXISTS (SELECT NULL FROM sys.extended_properties ep
WHERE ep.major_id = OBJECT_ID('%(table)s')
AND ep.name = 'MS_Description'
AND ep.minor_id = (SELECT column_id FROM sys.columns
WHERE name = '%(column)s'
AND object_id = OBJECT_ID('%(table)s')))
EXECUTE sp_addextendedproperty
@name = 'MS_Description', @value = %(comment)s,
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = %(table)s,
@level2type = 'COLUMN', @level2name = %(column)s
ELSE
EXECUTE sp_updateextendedproperty
@name = 'MS_Description', @value = %(comment)s,
@level0type = 'SCHEMA', @level0name = 'dbo',
@level1type = 'TABLE', @level1name = %(table)s,
@level2type = 'COLUMN', @level2name = %(column)s
"""
_deferred_unique_indexes = defaultdict(list)

def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
Expand Down Expand Up @@ -138,7 +171,18 @@ def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
},
params,
)


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": new_field.column,
"comment": self._comment_sql(new_db_comment),
},
[],
)

def _alter_column_null_sql(self, model, old_field, new_field):
"""
Hook to specialize column null alteration.
Expand Down Expand Up @@ -316,7 +360,19 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,

# Drop any FK constraints, we'll remake them later
fks_dropped = set()
if old_field.remote_field and old_field.db_constraint:
if (
old_field.remote_field
and old_field.db_constraint
and (django_version < (4,2)
or
(django_version >= (4, 2)
and self._field_should_be_altered(
old_field,
new_field,
ignore={"db_comment"})
)
)
):
# Drop index, SQL Server requires explicit deletion
if not hasattr(new_field, 'db_constraint') or not new_field.db_constraint:
index_names = self._constraint_names(model, [old_field.column], index=True)
Expand Down Expand Up @@ -446,8 +502,11 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
actions = []
null_actions = []
post_actions = []
# Type change?
if old_type != new_type:
# Type or comment change?
if old_type != new_type or (django_version >= (4, 2) and
self.connection.features.supports_comments
and old_field.db_comment != new_field.db_comment
):
if django_version >= (4, 2):
fragment, other_actions = self._alter_column_type_sql(
model, old_field, new_field, new_type, old_collation=None, new_collation=None
Expand Down Expand Up @@ -922,6 +981,19 @@ def add_field(self, model, field):
"changes": changes_sql,
}
self.execute(sql, params)
# Add field comment, if required.
if django_version >= (4, 2):
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))
# Add any FK constraints later
Expand Down Expand Up @@ -1129,6 +1201,23 @@ def create_model(self, model):
# Prevent using [] as params, in the case a literal '%' is used in the definition
self.execute(sql, params or None)

if django_version >= (4, 2) and 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 SQLite3 _remake_table needs it)
self.deferred_sql.extend(self._model_indexes_sql(model))
self.deferred_sql = list(set(self.deferred_sql))
Expand Down