From 5a50494995513de0148f7ebc94fa7c5c5fa273db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kawula?= Date: Sat, 16 Aug 2025 15:11:51 +0200 Subject: [PATCH 1/2] FIX: fixed error when subtracting DateTime fields (#368) fixed with same methodology as in django ticket https://github.com/microsoft/mssql-django/issues/368 --- mssql/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mssql/operations.py b/mssql/operations.py index 571c20f7..a50d31dc 100644 --- a/mssql/operations.py +++ b/mssql/operations.py @@ -556,12 +556,12 @@ def subtract_temporals(self, internal_type, lhs, rhs): rhs_sql, rhs_params = rhs if internal_type == 'DateField': sql = "CAST(DATEDIFF(day, %(rhs)s, %(lhs)s) AS bigint) * 86400 * 1000000" - params = rhs_params + lhs_params + params = (*rhs_params, *lhs_params) else: SECOND = "DATEDIFF(second, %(rhs)s, %(lhs)s)" MICROSECOND = "DATEPART(microsecond, %(lhs)s) - DATEPART(microsecond, %(rhs)s)" sql = "CAST({} AS bigint) * 1000000 + {}".format(SECOND, MICROSECOND) - params = rhs_params + lhs_params * 2 + rhs_params + params = tuple(rhs_params) + tuple(lhs_params) * 2 + tuple(rhs_params) return sql % {'lhs': lhs_sql, 'rhs': rhs_sql}, params def tablespace_sql(self, tablespace, inline=False): From e65d99adc4103a575d6accd2639c059c4994a35b Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Wed, 22 Apr 2026 06:27:54 +0000 Subject: [PATCH 2/2] FIX: Normalize params to tuples in subtract_temporals (#368) - Convert lhs_params/rhs_params to tuples early to prevent TypeError when compiler.compile() returns mixed list/tuple param types - Remove redundant tuple() calls in both DateField and else branches - Add comment explaining the normalization - Add 8 unit tests covering: both-tuples, both-lists, mixed list+tuple, empty params, DateField path, and DateTimeField path --- mssql/operations.py | 6 ++- testapp/tests/test_expressions.py | 83 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/mssql/operations.py b/mssql/operations.py index f6a59fc7..de035e51 100644 --- a/mssql/operations.py +++ b/mssql/operations.py @@ -567,16 +567,18 @@ def start_transaction_sql(self): def subtract_temporals(self, internal_type, lhs, rhs): lhs_sql, lhs_params = lhs rhs_sql, rhs_params = rhs + # Normalize to tuples so mixed list/tuple concatenation never fails + # (compiler.compile() may return either type for params). lhs_params = tuple(lhs_params) rhs_params = tuple(rhs_params) if internal_type == 'DateField': sql = "CAST(DATEDIFF(day, %(rhs)s, %(lhs)s) AS bigint) * 86400 * 1000000" - params = (*rhs_params, *lhs_params) + params = rhs_params + lhs_params else: SECOND = "DATEDIFF(second, %(rhs)s, %(lhs)s)" MICROSECOND = "DATEPART(microsecond, %(lhs)s) - DATEPART(microsecond, %(rhs)s)" sql = "CAST({} AS bigint) * 1000000 + {}".format(SECOND, MICROSECOND) - params = tuple(rhs_params) + tuple(lhs_params) * 2 + tuple(rhs_params) + params = rhs_params + lhs_params * 2 + rhs_params return sql % {'lhs': lhs_sql, 'rhs': rhs_sql}, params def tablespace_sql(self, tablespace, inline=False): diff --git a/testapp/tests/test_expressions.py b/testapp/tests/test_expressions.py index a4339a5d..155ceb40 100644 --- a/testapp/tests/test_expressions.py +++ b/testapp/tests/test_expressions.py @@ -214,3 +214,86 @@ def test_stringagg_order_by_outerref_does_not_use_within_group(self): self.assertEqual(values, ['Alpha']) self.assertNotIn('WITHIN GROUP', ctx[0]['sql']) + + +class TestSubtractTemporals(TestCase): + """ + Regression tests for subtract_temporals() handling mixed list/tuple params. + See https://github.com/microsoft/mssql-django/issues/368 + """ + + def _get_ops(self): + from django.db import connection + return connection.ops + + def test_date_field_both_tuples(self): + ops = self._get_ops() + lhs = ('%s', ('2024-01-15',)) + rhs = ('%s', ('2024-01-01',)) + sql, params = ops.subtract_temporals('DateField', lhs, rhs) + self.assertIn('DATEDIFF', sql) + self.assertEqual(params, ('2024-01-01', '2024-01-15')) + + def test_date_field_both_lists(self): + ops = self._get_ops() + lhs = ('%s', ['2024-01-15']) + rhs = ('%s', ['2024-01-01']) + sql, params = ops.subtract_temporals('DateField', lhs, rhs) + self.assertEqual(params, ('2024-01-01', '2024-01-15')) + + def test_date_field_mixed_list_and_tuple(self): + """The exact scenario from issue #368: list + empty tuple.""" + ops = self._get_ops() + lhs = ('%s', ()) # column ref with no params (tuple) + rhs = ('%s', ['2015-01-01']) # constant value (list) + sql, params = ops.subtract_temporals('DateField', lhs, rhs) + self.assertEqual(params, ('2015-01-01',)) + + def test_date_field_mixed_tuple_and_list(self): + ops = self._get_ops() + lhs = ('%s', ['2024-06-15']) + rhs = ('%s', ()) + sql, params = ops.subtract_temporals('DateField', lhs, rhs) + self.assertEqual(params, ('2024-06-15',)) + + def test_datetime_field_both_tuples(self): + ops = self._get_ops() + lhs = ('%s', ('2024-01-15 12:00:00',)) + rhs = ('%s', ('2024-01-01 00:00:00',)) + sql, params = ops.subtract_temporals('DateTimeField', lhs, rhs) + self.assertIn('DATEDIFF', sql) + # Pattern: rhs + lhs*2 + rhs + self.assertEqual(params, ( + '2024-01-01 00:00:00', + '2024-01-15 12:00:00', '2024-01-15 12:00:00', + '2024-01-01 00:00:00', + )) + + def test_datetime_field_mixed_list_and_tuple(self): + ops = self._get_ops() + lhs = ('%s', ()) + rhs = ('%s', ['2024-01-01 00:00:00']) + sql, params = ops.subtract_temporals('DateTimeField', lhs, rhs) + # rhs + lhs*2 + rhs = ('2024-01-01',) + () + ('2024-01-01',) + self.assertEqual(params, ('2024-01-01 00:00:00', '2024-01-01 00:00:00')) + + def test_datetime_field_both_lists(self): + ops = self._get_ops() + lhs = ('%s', ['2024-01-15 12:00:00']) + rhs = ('%s', ['2024-01-01 00:00:00']) + sql, params = ops.subtract_temporals('DateTimeField', lhs, rhs) + self.assertEqual(params, ( + '2024-01-01 00:00:00', + '2024-01-15 12:00:00', '2024-01-15 12:00:00', + '2024-01-01 00:00:00', + )) + + def test_date_field_no_params(self): + """Both sides are column references (no params).""" + ops = self._get_ops() + lhs = ('[t].[start_date]', ()) + rhs = ('[t].[end_date]', ()) + sql, params = ops.subtract_temporals('DateField', lhs, rhs) + self.assertEqual(params, ()) + self.assertIn('[t].[start_date]', sql) + self.assertIn('[t].[end_date]', sql)