From 802b65d12c817817eeb3670e89d40013222700f6 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 14 Apr 2026 13:03:01 +0530 Subject: [PATCH 1/5] FIX: Setinputsizes() SQL_DECIMAL crash --- mssql_python/cursor.py | 23 ++++++- tests/test_004_cursor.py | 125 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index ba5065d56..9f3efc029 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -885,8 +885,8 @@ def _get_c_type_for_sql_type(self, sql_type: int) -> int: ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, - ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value, - ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value, + ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_CHAR.value, + ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_CHAR.value, ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value, ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value, ddbc_sql_const.SQL_SMALLINT.value: ddbc_sql_const.SQL_C_SHORT.value, @@ -949,6 +949,16 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many # For non-NULL parameters, determine the appropriate C type based on SQL type c_type = self._get_c_type_for_sql_type(sql_type) + # Convert Decimal to string for SQL_C_CHAR binding (GH-503) + if ( + isinstance(parameter, decimal.Decimal) + and sql_type in ( + ddbc_sql_const.SQL_DECIMAL.value, + ddbc_sql_const.SQL_NUMERIC.value, + ) + ): + parameters_list[i] = format(parameter, "f") + # Check if this should be a DAE (data at execution) parameter # For string types with large column sizes if isinstance(parameter, str) and column_size > MAX_INLINE_CHAR: @@ -2306,6 +2316,15 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value ): processed_row[i] = format(val, "f") + # Convert Decimal to string for SQL_C_CHAR binding (GH-503) + elif ( + isinstance(val, decimal.Decimal) + and parameters_type[i].paramSQLType in ( + ddbc_sql_const.SQL_DECIMAL.value, + ddbc_sql_const.SQL_NUMERIC.value, + ) + ): + processed_row[i] = format(val, "f") # Existing numeric conversion elif parameters_type[i].paramSQLType in ( ddbc_sql_const.SQL_DECIMAL.value, diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 6ac157389..a450d3db8 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -9974,6 +9974,131 @@ def test_cursor_setinputsizes_with_executemany_float(db_connection): cursor.execute("DROP TABLE IF EXISTS #test_inputsizes_float") +def test_setinputsizes_sql_decimal_with_executemany(db_connection): + """Test setinputsizes with SQL_DECIMAL accepts Python Decimal values (GH-503). + + Without this fix, passing SQL_DECIMAL or SQL_NUMERIC via setinputsizes() + caused a RuntimeError because Decimal objects were not converted to + NumericData before the C binding validated the C type. + """ + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal") + cursor.execute(""" + CREATE TABLE #test_sis_decimal ( + Name NVARCHAR(100), + CategoryID INT, + Price DECIMAL(18,2) + ) + """) + + cursor.setinputsizes([ + (mssql_python.SQL_WVARCHAR, 100, 0), + (mssql_python.SQL_INTEGER, 0, 0), + (mssql_python.SQL_DECIMAL, 18, 2), + ]) + + cursor.executemany( + "INSERT INTO #test_sis_decimal (Name, CategoryID, Price) VALUES (?, ?, ?)", + [ + ("Widget", 1, decimal.Decimal("19.99")), + ("Gadget", 2, decimal.Decimal("29.99")), + ("Gizmo", 3, decimal.Decimal("0.01")), + ], + ) + + cursor.execute("SELECT Name, CategoryID, Price FROM #test_sis_decimal ORDER BY CategoryID") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == "Widget" + assert rows[0][1] == 1 + assert rows[0][2] == decimal.Decimal("19.99") + assert rows[1][0] == "Gadget" + assert rows[1][1] == 2 + assert rows[1][2] == decimal.Decimal("29.99") + assert rows[2][0] == "Gizmo" + assert rows[2][1] == 3 + assert rows[2][2] == decimal.Decimal("0.01") + + cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal") + + +def test_setinputsizes_sql_numeric_with_executemany(db_connection): + """Test setinputsizes with SQL_NUMERIC accepts Python Decimal values (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") + cursor.execute(""" + CREATE TABLE #test_sis_numeric ( + Value NUMERIC(10,4) + ) + """) + + cursor.setinputsizes([ + (mssql_python.SQL_NUMERIC, 10, 4), + ]) + + cursor.executemany( + "INSERT INTO #test_sis_numeric (Value) VALUES (?)", + [ + (decimal.Decimal("123.4567"),), + (decimal.Decimal("-99.0001"),), + (decimal.Decimal("0.0000"),), + ], + ) + + cursor.execute("SELECT Value FROM #test_sis_numeric ORDER BY Value") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == decimal.Decimal("-99.0001") + assert rows[1][0] == decimal.Decimal("0.0000") + assert rows[2][0] == decimal.Decimal("123.4567") + + cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") + + +def test_setinputsizes_sql_decimal_with_execute(db_connection): + """Test setinputsizes with SQL_DECIMAL works with single execute() too (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec") + cursor.execute("CREATE TABLE #test_sis_dec_exec (Price DECIMAL(18,2))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + cursor.execute( + "INSERT INTO #test_sis_dec_exec (Price) VALUES (?)", + decimal.Decimal("99.95"), + ) + + cursor.execute("SELECT Price FROM #test_sis_dec_exec") + row = cursor.fetchone() + assert row[0] == decimal.Decimal("99.95") + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec") + + +def test_setinputsizes_sql_decimal_null(db_connection): + """Test setinputsizes with SQL_DECIMAL handles NULL values correctly (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") + cursor.execute("CREATE TABLE #test_sis_dec_null (Price DECIMAL(18,2))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + cursor.execute( + "INSERT INTO #test_sis_dec_null (Price) VALUES (?)", + None, + ) + + cursor.execute("SELECT Price FROM #test_sis_dec_null") + row = cursor.fetchone() + assert row[0] is None + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") + + def test_cursor_setinputsizes_reset(db_connection): """Test that setinputsizes is reset after execution""" From 85fc8b3bccdc1fbad34773412e3aa51bbc003794 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 14 Apr 2026 13:16:57 +0530 Subject: [PATCH 2/5] Resolving commits --- mssql_python/cursor.py | 38 ++++----- tests/test_004_cursor.py | 168 ++++++++++++++++++++------------------- 2 files changed, 103 insertions(+), 103 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 9f3efc029..722747c35 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -950,14 +950,12 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many c_type = self._get_c_type_for_sql_type(sql_type) # Convert Decimal to string for SQL_C_CHAR binding (GH-503) - if ( - isinstance(parameter, decimal.Decimal) - and sql_type in ( - ddbc_sql_const.SQL_DECIMAL.value, - ddbc_sql_const.SQL_NUMERIC.value, - ) + if isinstance(parameter, decimal.Decimal) and sql_type in ( + ddbc_sql_const.SQL_DECIMAL.value, + ddbc_sql_const.SQL_NUMERIC.value, ): parameters_list[i] = format(parameter, "f") + parameter = parameters_list[i] # Check if this should be a DAE (data at execution) parameter # For string types with large column sizes @@ -2316,26 +2314,20 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value ): processed_row[i] = format(val, "f") - # Convert Decimal to string for SQL_C_CHAR binding (GH-503) - elif ( - isinstance(val, decimal.Decimal) - and parameters_type[i].paramSQLType in ( - ddbc_sql_const.SQL_DECIMAL.value, - ddbc_sql_const.SQL_NUMERIC.value, - ) - ): - processed_row[i] = format(val, "f") - # Existing numeric conversion + # Convert all values to string for DECIMAL/NUMERIC columns (GH-503) elif parameters_type[i].paramSQLType in ( ddbc_sql_const.SQL_DECIMAL.value, ddbc_sql_const.SQL_NUMERIC.value, - ) and not isinstance(val, decimal.Decimal): - try: - processed_row[i] = decimal.Decimal(str(val)) - except Exception as e: # pylint: disable=broad-exception-caught - raise ValueError( - f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}" - ) from e + ): + if isinstance(val, decimal.Decimal): + processed_row[i] = format(val, "f") + else: + try: + processed_row[i] = format(decimal.Decimal(str(val)), "f") + except Exception as e: # pylint: disable=broad-exception-caught + raise ValueError( + f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}" + ) from e processed_parameters.append(processed_row) # Now transpose the processed parameters diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index a450d3db8..dd5333158 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -9984,44 +9984,47 @@ def test_setinputsizes_sql_decimal_with_executemany(db_connection): cursor = db_connection.cursor() cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal") - cursor.execute(""" - CREATE TABLE #test_sis_decimal ( - Name NVARCHAR(100), - CategoryID INT, - Price DECIMAL(18,2) - ) - """) - - cursor.setinputsizes([ - (mssql_python.SQL_WVARCHAR, 100, 0), - (mssql_python.SQL_INTEGER, 0, 0), - (mssql_python.SQL_DECIMAL, 18, 2), - ]) + try: + cursor.execute(""" + CREATE TABLE #test_sis_decimal ( + Name NVARCHAR(100), + CategoryID INT, + Price DECIMAL(18,2) + ) + """) - cursor.executemany( - "INSERT INTO #test_sis_decimal (Name, CategoryID, Price) VALUES (?, ?, ?)", - [ - ("Widget", 1, decimal.Decimal("19.99")), - ("Gadget", 2, decimal.Decimal("29.99")), - ("Gizmo", 3, decimal.Decimal("0.01")), - ], - ) + cursor.setinputsizes( + [ + (mssql_python.SQL_WVARCHAR, 100, 0), + (mssql_python.SQL_INTEGER, 0, 0), + (mssql_python.SQL_DECIMAL, 18, 2), + ] + ) - cursor.execute("SELECT Name, CategoryID, Price FROM #test_sis_decimal ORDER BY CategoryID") - rows = cursor.fetchall() + cursor.executemany( + "INSERT INTO #test_sis_decimal (Name, CategoryID, Price) VALUES (?, ?, ?)", + [ + ("Widget", 1, decimal.Decimal("19.99")), + ("Gadget", 2, decimal.Decimal("29.99")), + ("Gizmo", 3, decimal.Decimal("0.01")), + ], + ) - assert len(rows) == 3 - assert rows[0][0] == "Widget" - assert rows[0][1] == 1 - assert rows[0][2] == decimal.Decimal("19.99") - assert rows[1][0] == "Gadget" - assert rows[1][1] == 2 - assert rows[1][2] == decimal.Decimal("29.99") - assert rows[2][0] == "Gizmo" - assert rows[2][1] == 3 - assert rows[2][2] == decimal.Decimal("0.01") + cursor.execute("SELECT Name, CategoryID, Price FROM #test_sis_decimal ORDER BY CategoryID") + rows = cursor.fetchall() - cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal") + assert len(rows) == 3 + assert rows[0][0] == "Widget" + assert rows[0][1] == 1 + assert rows[0][2] == decimal.Decimal("19.99") + assert rows[1][0] == "Gadget" + assert rows[1][1] == 2 + assert rows[1][2] == decimal.Decimal("29.99") + assert rows[2][0] == "Gizmo" + assert rows[2][1] == 3 + assert rows[2][2] == decimal.Decimal("0.01") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal") def test_setinputsizes_sql_numeric_with_executemany(db_connection): @@ -10029,34 +10032,37 @@ def test_setinputsizes_sql_numeric_with_executemany(db_connection): cursor = db_connection.cursor() cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") - cursor.execute(""" - CREATE TABLE #test_sis_numeric ( - Value NUMERIC(10,4) - ) - """) - - cursor.setinputsizes([ - (mssql_python.SQL_NUMERIC, 10, 4), - ]) + try: + cursor.execute(""" + CREATE TABLE #test_sis_numeric ( + Value NUMERIC(10,4) + ) + """) - cursor.executemany( - "INSERT INTO #test_sis_numeric (Value) VALUES (?)", - [ - (decimal.Decimal("123.4567"),), - (decimal.Decimal("-99.0001"),), - (decimal.Decimal("0.0000"),), - ], - ) + cursor.setinputsizes( + [ + (mssql_python.SQL_NUMERIC, 10, 4), + ] + ) - cursor.execute("SELECT Value FROM #test_sis_numeric ORDER BY Value") - rows = cursor.fetchall() + cursor.executemany( + "INSERT INTO #test_sis_numeric (Value) VALUES (?)", + [ + (decimal.Decimal("123.4567"),), + (decimal.Decimal("-99.0001"),), + (decimal.Decimal("0.0000"),), + ], + ) - assert len(rows) == 3 - assert rows[0][0] == decimal.Decimal("-99.0001") - assert rows[1][0] == decimal.Decimal("0.0000") - assert rows[2][0] == decimal.Decimal("123.4567") + cursor.execute("SELECT Value FROM #test_sis_numeric ORDER BY Value") + rows = cursor.fetchall() - cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") + assert len(rows) == 3 + assert rows[0][0] == decimal.Decimal("-99.0001") + assert rows[1][0] == decimal.Decimal("0.0000") + assert rows[2][0] == decimal.Decimal("123.4567") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") def test_setinputsizes_sql_decimal_with_execute(db_connection): @@ -10064,19 +10070,20 @@ def test_setinputsizes_sql_decimal_with_execute(db_connection): cursor = db_connection.cursor() cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec") - cursor.execute("CREATE TABLE #test_sis_dec_exec (Price DECIMAL(18,2))") - - cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) - cursor.execute( - "INSERT INTO #test_sis_dec_exec (Price) VALUES (?)", - decimal.Decimal("99.95"), - ) + try: + cursor.execute("CREATE TABLE #test_sis_dec_exec (Price DECIMAL(18,2))") - cursor.execute("SELECT Price FROM #test_sis_dec_exec") - row = cursor.fetchone() - assert row[0] == decimal.Decimal("99.95") + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + cursor.execute( + "INSERT INTO #test_sis_dec_exec (Price) VALUES (?)", + decimal.Decimal("99.95"), + ) - cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec") + cursor.execute("SELECT Price FROM #test_sis_dec_exec") + row = cursor.fetchone() + assert row[0] == decimal.Decimal("99.95") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec") def test_setinputsizes_sql_decimal_null(db_connection): @@ -10084,19 +10091,20 @@ def test_setinputsizes_sql_decimal_null(db_connection): cursor = db_connection.cursor() cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") - cursor.execute("CREATE TABLE #test_sis_dec_null (Price DECIMAL(18,2))") - - cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) - cursor.execute( - "INSERT INTO #test_sis_dec_null (Price) VALUES (?)", - None, - ) + try: + cursor.execute("CREATE TABLE #test_sis_dec_null (Price DECIMAL(18,2))") - cursor.execute("SELECT Price FROM #test_sis_dec_null") - row = cursor.fetchone() - assert row[0] is None + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + cursor.execute( + "INSERT INTO #test_sis_dec_null (Price) VALUES (?)", + None, + ) - cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") + cursor.execute("SELECT Price FROM #test_sis_dec_null") + row = cursor.fetchone() + assert row[0] is None + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") def test_cursor_setinputsizes_reset(db_connection): From 91e67e596a4d1572d38055deeeae07c863e9adc5 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 14 Apr 2026 13:45:51 +0530 Subject: [PATCH 3/5] Increasing code coverage --- tests/test_004_cursor.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index dd5333158..07b087efc 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10065,6 +10065,33 @@ def test_setinputsizes_sql_numeric_with_executemany(db_connection): cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric") +def test_setinputsizes_sql_decimal_with_non_decimal_values(db_connection): + """Test setinputsizes with SQL_DECIMAL converts non-Decimal values (int/float) to string (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_nondec") + try: + cursor.execute("CREATE TABLE #test_sis_dec_nondc (Price DECIMAL(18,2))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + + # Pass int and float instead of Decimal — exercises the non-Decimal conversion branch + cursor.executemany( + "INSERT INTO #test_sis_dec_nondc (Price) VALUES (?)", + [(42,), (19.99,), (0,)], + ) + + cursor.execute("SELECT Price FROM #test_sis_dec_nondc ORDER BY Price") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == decimal.Decimal("0.00") + assert rows[1][0] == decimal.Decimal("19.99") + assert rows[2][0] == decimal.Decimal("42.00") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_nondc") + + def test_setinputsizes_sql_decimal_with_execute(db_connection): """Test setinputsizes with SQL_DECIMAL works with single execute() too (GH-503).""" cursor = db_connection.cursor() From c4da0d9472fa5a84aa1b4f9858beb123867a524b Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 21 Apr 2026 13:09:09 +0530 Subject: [PATCH 4/5] Resolving comments --- mssql_python/cursor.py | 25 ++++++-- tests/test_004_cursor.py | 130 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 6 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 722747c35..3f21d0b6d 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -885,8 +885,8 @@ def _get_c_type_for_sql_type(self, sql_type: int) -> int: ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value, - ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_CHAR.value, - ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_CHAR.value, + ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value, + ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value, ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value, ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value, ddbc_sql_const.SQL_SMALLINT.value: ddbc_sql_const.SQL_C_SHORT.value, @@ -949,13 +949,19 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many # For non-NULL parameters, determine the appropriate C type based on SQL type c_type = self._get_c_type_for_sql_type(sql_type) - # Convert Decimal to string for SQL_C_CHAR binding (GH-503) - if isinstance(parameter, decimal.Decimal) and sql_type in ( + # Override DECIMAL/NUMERIC to use SQL_C_CHAR string binding (GH-503). + # The generic mapping returns SQL_C_NUMERIC which requires NumericData + # structs, but setinputsizes declares fixed precision/scale that may + # differ from per-value precision, causing misinterpretation. String + # binding lets ODBC convert using the declared columnSize/decimalDigits. + if sql_type in ( ddbc_sql_const.SQL_DECIMAL.value, ddbc_sql_const.SQL_NUMERIC.value, ): - parameters_list[i] = format(parameter, "f") - parameter = parameters_list[i] + c_type = ddbc_sql_const.SQL_C_CHAR.value + if isinstance(parameter, decimal.Decimal): + parameters_list[i] = format(parameter, "f") + parameter = parameters_list[i] # Check if this should be a DAE (data at execution) parameter # For string types with large column sizes @@ -2185,6 +2191,13 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s # Determine appropriate C type based on SQL type c_type = self._get_c_type_for_sql_type(sql_type) + # Override DECIMAL/NUMERIC to use SQL_C_CHAR string binding (GH-503) + if sql_type in ( + ddbc_sql_const.SQL_DECIMAL.value, + ddbc_sql_const.SQL_NUMERIC.value, + ): + c_type = ddbc_sql_const.SQL_C_CHAR.value + # Check if this should be a DAE (data at execution) parameter based on column size if sample_value is not None: if isinstance(sample_value, str) and column_size > MAX_INLINE_CHAR: diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 07b087efc..9508bca83 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10134,6 +10134,136 @@ def test_setinputsizes_sql_decimal_null(db_connection): cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null") +def test_setinputsizes_sql_decimal_unconvertible_value(db_connection): + """Test setinputsizes with SQL_DECIMAL raises ValueError for unconvertible values (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_bad") + try: + cursor.execute("CREATE TABLE #test_sis_dec_bad (Price DECIMAL(18,2))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + + with pytest.raises(ValueError, match="Failed to convert parameter"): + cursor.executemany( + "INSERT INTO #test_sis_dec_bad (Price) VALUES (?)", + [("not_a_number",)], + ) + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_bad") + + +def test_setinputsizes_sql_decimal_high_precision(db_connection): + """Test setinputsizes with SQL_DECIMAL preserves full DECIMAL(38,18) precision (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_hp") + try: + cursor.execute("CREATE TABLE #test_sis_dec_hp (Value DECIMAL(38,18))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 38, 18)]) + + high_prec = decimal.Decimal("12345678901234567890.123456789012345678") + cursor.execute( + "INSERT INTO #test_sis_dec_hp (Value) VALUES (?)", + high_prec, + ) + + cursor.execute("SELECT Value FROM #test_sis_dec_hp") + row = cursor.fetchone() + assert row[0] == high_prec + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_hp") + + +def test_setinputsizes_sql_decimal_negative_zero(db_connection): + """Test setinputsizes with SQL_DECIMAL handles Decimal('-0.00') correctly (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_negz") + try: + cursor.execute("CREATE TABLE #test_sis_dec_negz (Value DECIMAL(18,2))") + + cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)]) + cursor.execute( + "INSERT INTO #test_sis_dec_negz (Value) VALUES (?)", + decimal.Decimal("-0.00"), + ) + + cursor.execute("SELECT Value FROM #test_sis_dec_negz") + row = cursor.fetchone() + # SQL Server normalizes -0.00 to 0.00 + assert row[0] == decimal.Decimal("0.00") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_negz") + + +def test_setinputsizes_sql_decimal_mixed_null_executemany(db_connection): + """Test setinputsizes with SQL_DECIMAL handles mixed NULL/non-NULL in executemany (GH-503).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_mix") + try: + cursor.execute("CREATE TABLE #test_sis_dec_mix (Id INT, Price DECIMAL(18,2))") + + cursor.setinputsizes([ + (mssql_python.SQL_INTEGER, 0, 0), + (mssql_python.SQL_DECIMAL, 18, 2), + ]) + + cursor.executemany( + "INSERT INTO #test_sis_dec_mix (Id, Price) VALUES (?, ?)", + [ + (1, decimal.Decimal("10.50")), + (2, None), + (3, decimal.Decimal("30.75")), + (4, None), + ], + ) + + cursor.execute("SELECT Id, Price FROM #test_sis_dec_mix ORDER BY Id") + rows = cursor.fetchall() + + assert len(rows) == 4 + assert rows[0][1] == decimal.Decimal("10.50") + assert rows[1][1] is None + assert rows[2][1] == decimal.Decimal("30.75") + assert rows[3][1] is None + finally: + cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_mix") + + +def test_decimal_without_setinputsizes_no_regression(db_connection): + """Verify plain Decimal binding without setinputsizes still works (GH-503 regression check).""" + cursor = db_connection.cursor() + + cursor.execute("DROP TABLE IF EXISTS #test_dec_noreg") + try: + cursor.execute("CREATE TABLE #test_dec_noreg (Price DECIMAL(18,2))") + + # Single execute without setinputsizes + cursor.execute( + "INSERT INTO #test_dec_noreg (Price) VALUES (?)", + decimal.Decimal("49.99"), + ) + + # executemany without setinputsizes + cursor.executemany( + "INSERT INTO #test_dec_noreg (Price) VALUES (?)", + [(decimal.Decimal("99.99"),), (decimal.Decimal("0.01"),)], + ) + + cursor.execute("SELECT Price FROM #test_dec_noreg ORDER BY Price") + rows = cursor.fetchall() + + assert len(rows) == 3 + assert rows[0][0] == decimal.Decimal("0.01") + assert rows[1][0] == decimal.Decimal("49.99") + assert rows[2][0] == decimal.Decimal("99.99") + finally: + cursor.execute("DROP TABLE IF EXISTS #test_dec_noreg") + + def test_cursor_setinputsizes_reset(db_connection): """Test that setinputsizes is reset after execution""" From 632f83fe365db2ff1acfec32abe44e1ff2d78204 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 21 Apr 2026 13:59:22 +0530 Subject: [PATCH 5/5] resolving linting issue --- tests/test_004_cursor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 9508bca83..2f60aaad9 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10206,10 +10206,12 @@ def test_setinputsizes_sql_decimal_mixed_null_executemany(db_connection): try: cursor.execute("CREATE TABLE #test_sis_dec_mix (Id INT, Price DECIMAL(18,2))") - cursor.setinputsizes([ - (mssql_python.SQL_INTEGER, 0, 0), - (mssql_python.SQL_DECIMAL, 18, 2), - ]) + cursor.setinputsizes( + [ + (mssql_python.SQL_INTEGER, 0, 0), + (mssql_python.SQL_DECIMAL, 18, 2), + ] + ) cursor.executemany( "INSERT INTO #test_sis_dec_mix (Id, Price) VALUES (?, ?)",