From 802b65d12c817817eeb3670e89d40013222700f6 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 14 Apr 2026 13:03:01 +0530 Subject: [PATCH 1/3] 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/3] 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/3] 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()