microsoft/mssql-django

Having both a CTE, a GROUP BY and parameters causes crashes.

nils-van-zuijlen opened this issue · 1 comments

There are some features which are not supported yet. Please check the Limitations first to see if your bug is listed.

Software versions

  • Django: 4.2
  • mssql-django: 1.5
  • python: 3.11.8
  • SQL Server: 14.0.0
  • OS: Linux 6.6.36 NixOS

Table schema and Model

Database Connection Settings

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "master",
        "USER": "sa",
        "PASSWORD": "REDACTED",
        "HOST": "127.0.0.1",
        "PORT": 1433,
        "OPTIONS": {
            # This database doesn't have any triggers so can use return
            # rows from bulk insert feature
            "return_rows_bulk_insert": True
        },
    }
}

Problem description and steps to reproduce

Using a GROUP BY clause in a query that begins with a CTE (WITH ...) and that has parameters causes a crash:

ProgrammingError: ('42000', "[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. (319) (SQLExecDirectW)")

The following code will trigger the behaviour:

from django.db import DEFAULT_DB_ALIAS, connections

with connections[DEFAULT_DB_ALIAS].cursor() as cursor:
    cursor.execute("""
        WITH a AS (SELECT 1 as b)
        SELECT b FROM a
        WHERE b = %s
        GROUP BY b
    """, [1])

Expected behavior and actual behavior

I expect the query to be executed without issues.

Error message/stack trace

In [1]: from django.db import DEFAULT_DB_ALIAS, connections

In [2]: with connections[DEFAULT_DB_ALIAS].cursor() as cursor:
   ...:         cursor.execute(
   ...:         "WITH a AS (SELECT 1 as b) SELECT b FROM a WHERE b = %s GROUP BY b",
   ...:         [1])
   ...: 
---------------------------------------------------------------------------
ProgrammingError                          Traceback (most recent call last)
File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:89, in CursorWrapper._execute(self, sql, params, *ignored_wrapper_args)
     88 else:
---> 89     return self.cursor.execute(sql, params)

File ~/Documents/project/.venv/lib/python3.11/site-packages/mssql/base.py:677, in CursorWrapper.execute(self, sql, params)
    676 try:
--> 677     return self.cursor.execute(sql, params)
    678 except Database.Error as e:

ProgrammingError: ('42000', "[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. (319) (SQLExecDirectW)")

The above exception was the direct cause of the following exception:

ProgrammingError                          Traceback (most recent call last)
Cell In[2], line 2
      1 with connections[DEFAULT_DB_ALIAS].cursor() as cursor:
----> 2         cursor.execute(
      3         "WITH a AS (SELECT 1 as b) SELECT b FROM a WHERE b = %s GROUP BY b",
      4         [1])

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:102, in CursorDebugWrapper.execute(self, sql, params)
    100 def execute(self, sql, params=None):
    101     with self.debug_sql(sql, params, use_last_executed_query=True):
--> 102         return super().execute(sql, params)

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:67, in CursorWrapper.execute(self, sql, params)
     66 def execute(self, sql, params=None):
---> 67     return self._execute_with_wrappers(
     68         sql, params, many=False, executor=self._execute
     69     )

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:80, in CursorWrapper._execute_with_wrappers(self, sql, params, many, executor)
     78 for wrapper in reversed(self.db.execute_wrappers):
     79     executor = functools.partial(wrapper, executor)
---> 80 return executor(sql, params, many, context)

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:84, in CursorWrapper._execute(self, sql, params, *ignored_wrapper_args)
     82 def _execute(self, sql, params, *ignored_wrapper_args):
     83     self.db.validate_no_broken_transaction()
---> 84     with self.db.wrap_database_errors:
     85         if params is None:
     86             # params default might be backend specific.
     87             return self.cursor.execute(sql)

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/utils.py:91, in DatabaseErrorWrapper.__exit__(self, exc_type, exc_value, traceback)
     89 if dj_exc_type not in (DataError, IntegrityError):
     90     self.wrapper.errors_occurred = True
---> 91 raise dj_exc_value.with_traceback(traceback) from exc_value

File ~/Documents/project/.venv/lib/python3.11/site-packages/django/db/backends/utils.py:89, in CursorWrapper._execute(self, sql, params, *ignored_wrapper_args)
     87     return self.cursor.execute(sql)
     88 else:
---> 89     return self.cursor.execute(sql, params)

File ~/Documents/project/.venv/lib/python3.11/site-packages/mssql/base.py:677, in CursorWrapper.execute(self, sql, params)
    675 self.last_params = params
    676 try:
--> 677     return self.cursor.execute(sql, params)
    678 except Database.Error as e:
    679     self.connection._on_error(e)

ProgrammingError: ('42000', "[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. (319) (SQLExecDirectW)")

Any other details that can be helpful

This is caused by the variable declaration added in #354. I think the new code path should not be used for raw queries, because if a raw query runs into the issue fixed by that PR, it can be fixed by manually adding the variable declaration.

Thanks for bringing this up. Will be looking into this further