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

schema support #281

Open
wants to merge 52 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
eec6218
added schema support
Jun 20, 2023
0a895a8
Update README.md
nasreddine27 Jun 20, 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
05329e6
rename table fix
nasreddine27 Jul 24, 2023
9c3be2b
add partial support for adding/altering comments on columns and tables
maikhanhbui Jul 25, 2023
2e5bd44
Merge branch 'dev' into dev
nasreddine27 Jul 27, 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
a7a6bf7
Merge branch 'dev' into dev
nasreddine27 Aug 2, 2023
6410404
Merge branch 'dev' into dev
nasreddine27 Oct 24, 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
072ea26
Merge pull request #318 from microsoft/add-comments-support
dauinsight Dec 14, 2023
c8dbc14
Merge branch 'dev' into dev
nasreddine27 Dec 14, 2023
3aef1d9
Merge branch 'dev' into dev
nasreddine27 Jun 7, 2024
e11aaa7
Update settings.py to Allow db_table_schema to be added on migration …
nasreddine27 Jun 7, 2024
8dfffab
Update models.py
nasreddine27 Jun 7, 2024
0c2d035
Update compiler.py : treat each model separately when a a JOIN clause…
nasreddine27 Jun 7, 2024
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ in DATABASES control the behavior of the backend:
- PASSWORD

String. Database user password.

- SCHEMA

String. Default schema to use. Not required.

- TOKEN

String. Access token fetched as a user or service principal which
Expand Down
137 changes: 126 additions & 11 deletions mssql/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
if django.VERSION >= (4, 2):
from django.core.exceptions import EmptyResultSet, FullResultSet

from .introspection import get_table_name, get_schema_name
from django.apps import apps

def _as_sql_agv(self, compiler, connection):
return self.as_sql(compiler, connection, template='%(function)s(CONVERT(float, %(field)s))')

Expand Down Expand Up @@ -196,7 +199,6 @@ def _cursor_iter(cursor, sentinel, col_count, itersize):

compiler.cursor_iter = _cursor_iter


class SQLCompiler(compiler.SQLCompiler):

def as_sql(self, with_limits=True, with_col_aliases=False):
Expand Down Expand Up @@ -227,6 +229,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
do_offset_emulation = do_offset and not supports_offset_clause

if combinator:

if not getattr(features, 'supports_select_{}'.format(combinator)):
raise NotSupportedError('{} is not supported on this database backend.'.format(combinator))
result, params = self.get_combinator_sql(combinator, self.query.combinator_all)
Expand Down Expand Up @@ -285,7 +288,8 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
if do_offset:
meta = self.query.get_meta()
qn = self.quote_name_unless_alias
offsetting_order_by = '%s.%s' % (qn(meta.db_table), qn(meta.pk.db_column or meta.pk.column))
table = qn(get_table_name(self, meta.db_table, getattr(meta, "db_table_schema", False)))
offsetting_order_by = '%s.%s' % (table, qn(meta.pk.db_column or meta.pk.column))
if do_offset_emulation:
if order_by:
ordering = []
Expand Down Expand Up @@ -431,12 +435,53 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
', '.join(sub_selects),
' '.join(result),
), tuple(sub_params + params)

return ' '.join(result), tuple(params)
finally:
# Finally do cleanup - get rid of the joins we created above.
self.query.reset_refcounts(refcounts_before)

def get_from_clause(self):
"""
Return a list of strings that are joined together to go after the
"FROM" part of the query, as well as a list any extra parameters that
need to be included. Subclasses, can override this to create a
from-clause via a "select".

This should only be called after any SQL construction methods that
might change the tables that are needed. This means the select columns,
ordering, and distinct must be done first.
"""
result = []
params = []
for alias in tuple(self.query.alias_map):
if not self.query.alias_refcount[alias]:
continue
try:
from_clause = self.query.alias_map[alias]
except KeyError:
# Extra tables can end up in self.tables, but not in the
# alias_map if they aren't in a join. That's OK. We skip them.
continue
settings_dict = self.connection.settings_dict
clause_sql, clause_params = self.compile(from_clause)
model = next((m for m in apps.get_models() if m._meta.db_table == from_clause.table_name), None)
schema = getattr(getattr(model,"_meta", None), "db_table_schema", settings_dict.get('SCHEMA', False))
if schema:
if 'JOIN' in clause_sql:
table_clause_sql = clause_sql.split('JOIN ')[1].split(' ON')[0]
table_clause_sql = f'[{schema}].{table_clause_sql}'
clause_sql = clause_sql.split('JOIN')[0] + 'JOIN ' + table_clause_sql + ' ON' + clause_sql.split('JOIN')[1].split('ON')[1]
else:
clause_sql = f'[{schema}].{clause_sql}'
result.append(clause_sql)
params.extend(clause_params)
for t in self.query.extra_tables:
alias, _ = self.query.table_alias(t)
# Only add the alias if it's not already present (the table_alias()
# call increments the refcount, so an alias refcount of one means
# this is the only reference).
if alias not in self.query.alias_map or self.query.alias_refcount[alias] == 1:
result.append(', %s' % self.quote_name_unless_alias(alias))
return result, params
def compile(self, node, *args, **kwargs):
node = self._as_microsoft(node)
return super().compile(node, *args, **kwargs)
Expand Down Expand Up @@ -550,7 +595,7 @@ def fix_auto(self, sql, opts, fields, qn):
columns = [f.column for f in fields]
if auto_field_column in columns:
id_insert_sql = []
table = qn(opts.db_table)
table = qn(get_table_name(self, opts.db_table, getattr(opts, "db_table_schema", False)))
sql_format = 'SET IDENTITY_INSERT %s ON; %s; SET IDENTITY_INSERT %s OFF'
for q, p in sql:
id_insert_sql.append((sql_format % (table, q, table), p))
Expand Down Expand Up @@ -587,7 +632,8 @@ def as_sql(self):
# going to be column names (so we can avoid the extra overhead).
qn = self.connection.ops.quote_name
opts = self.query.get_meta()
result = ['INSERT INTO %s' % qn(opts.db_table)]
table = qn(get_table_name(self, opts.db_table, getattr(opts, "db_table_schema", False)))
result = ['INSERT INTO %s' % table]

if self.query.fields:
fields = self.query.fields
Expand Down Expand Up @@ -617,7 +663,7 @@ def as_sql(self):
# There isn't really a single statement to bulk multiple DEFAULT VALUES insertions,
# so we have to use a workaround:
# https://dba.stackexchange.com/questions/254771/insert-multiple-rows-into-a-table-with-only-an-identity-column
result = [self.bulk_insert_default_values_sql(qn(opts.db_table))]
result = [self.bulk_insert_default_values_sql(qn(table))]
r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields())
if r_sql:
result.append(r_sql)
Expand Down Expand Up @@ -660,13 +706,82 @@ def as_sql(self):
sql = '; '.join(['SET NOCOUNT OFF', sql])
return sql, params

def _as_sql(self, query):
opts = self.query.get_meta()
table = get_table_name(self, query.base_table, getattr(opts, "db_table_schema", False))
delete = "DELETE FROM %s" % self.quote_name_unless_alias(table)
try:
where, params = self.compile(query.where)
except FullResultSet:
return delete, ()
return f"{delete} WHERE {where}", tuple(params)


class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
def as_sql(self):
sql, params = super().as_sql()
if sql:
sql = '; '.join(['SET NOCOUNT OFF', sql])
return sql, params
"""
Create the SQL for this query. Return the SQL string and list of
parameters.
"""
self.pre_sql_setup()
if not self.query.values:
return "", ()
qn = self.quote_name_unless_alias
values, update_params = [], []
for field, model, val in self.query.values:
if hasattr(val, "resolve_expression"):
val = val.resolve_expression(
self.query, allow_joins=False, for_save=True
)
if val.contains_aggregate:
raise FieldError(
"Aggregate functions are not allowed in this query "
"(%s=%r)." % (field.name, val)
)
if val.contains_over_clause:
raise FieldError(
"Window expressions are not allowed in this query "
"(%s=%r)." % (field.name, val)
)
elif hasattr(val, "prepare_database_save"):
if field.remote_field:
val = val.prepare_database_save(field)
else:
raise TypeError(
"Tried to update field %s with a model instance, %r. "
"Use a value compatible with %s."
% (field, val, field.__class__.__name__)
)
val = field.get_db_prep_save(val, connection=self.connection)

# Getting the placeholder for the field.
if hasattr(field, "get_placeholder"):
placeholder = field.get_placeholder(val, self, self.connection)
else:
placeholder = "%s"
name = field.column
if hasattr(val, "as_sql"):
sql, params = self.compile(val)
values.append("%s = %s" % (qn(name), placeholder % sql))
update_params.extend(params)
elif val is not None:
values.append("%s = %s" % (qn(name), placeholder))
update_params.append(val)
else:
values.append("%s = NULL" % qn(name))
opts = self.query.get_meta()
table = get_table_name(self, self.query.base_table, getattr(opts, "db_table_schema", False))
result = [
"UPDATE %s SET" % qn(table),
", ".join(values),
]
try:
where, params = self.compile(self.query.where)
except FullResultSet:
params = []
else:
result.append("WHERE %s" % where)
return " ".join(result), tuple(update_params + params)


class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
Expand Down
33 changes: 27 additions & 6 deletions mssql/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,34 @@
SQL_BIGAUTOFIELD = -777444
SQL_SMALLAUTOFIELD = -777333
SQL_TIMESTAMP_WITH_TIMEZONE = -155
from django.db import connection

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

def get_schema_name():
return getattr(settings, 'SCHEMA_TO_INSPECT', 'SCHEMA_NAME()')
# get default schema choosen by user in settings.py else SCHEMA_NAME()
settings_dict = connection.settings_dict
schema = settings_dict.get('SCHEMA', False)
return f"'{schema}'" if schema else 'SCHEMA_NAME()'

def get_table_name(object, table_name, custom_schema):
"""
get the name of the table on this format schema].[table_name
if
schema = custom schema defined in medels meta (db_table_schema)
else
schema choosen by user in settings.py
else
return the name of the table without schema (defalut one will be used)
"""
if custom_schema:
return f'{custom_schema}].[{table_name}'
settings_dict = object.connection.settings_dict
schema_name = settings_dict.get('SCHEMA', False)
if schema_name:
return f'{schema_name}].[{table_name}'
return table_name

class DatabaseIntrospection(BaseDatabaseIntrospection):
# Map type codes to Django Field types.
Expand Down Expand Up @@ -237,7 +258,7 @@ def get_key_columns(self, cursor, table_name):
key_columns.extend([tuple(row) for row in cursor.fetchall()])
return key_columns

def get_constraints(self, cursor, table_name):
def get_constraints(self, cursor, table_name, table_name_schema='SCHEMA_NAME()'):
"""
Retrieves any constraints or keys (unique, pk, fk, check, index)
across one or more columns.
Expand Down Expand Up @@ -296,12 +317,12 @@ def get_constraints(self, cursor, table_name):
kc.table_name = fk.table_name AND
kc.column_name = fk.column_name
WHERE
kc.table_schema = {get_schema_name()} AND
kc.table_schema = {table_name_schema} AND
kc.table_name = %s
ORDER BY
kc.constraint_name ASC,
kc.ordinal_position ASC
""", [table_name])
""" , [table_name])
for constraint, column, kind, ref_table, ref_column in cursor.fetchall():
# If we're the first column, make the record
if constraint not in constraints:
Expand Down Expand Up @@ -331,7 +352,7 @@ def get_constraints(self, cursor, table_name):
kc.constraint_name = c.constraint_name
WHERE
c.constraint_type = 'CHECK' AND
kc.table_schema = {get_schema_name()} AND
kc.table_schema = {table_name_schema} AND
kc.table_name = %s
""", [table_name])
for constraint, column in cursor.fetchall():
Expand Down Expand Up @@ -398,7 +419,7 @@ def get_constraints(self, cursor, table_name):
ic.object_id = c.object_id AND
ic.column_id = c.column_id
WHERE
t.schema_id = SCHEMA_ID({get_schema_name()}) AND
t.schema_id = SCHEMA_ID({table_name_schema}) AND
t.name = %s
ORDER BY
i.index_id ASC,
Expand Down
Loading