Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 21 additions & 10 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -949,6 +949,14 @@ 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")
parameter = parameters_list[i]

# 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:
Comment thread
jahnvi480 marked this conversation as resolved.
Expand Down Expand Up @@ -2306,17 +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")
# 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,
Comment thread
jahnvi480 marked this conversation as resolved.
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
Expand Down
160 changes: 160 additions & 0 deletions tests/test_004_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9974,6 +9974,166 @@ 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")
try:
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")
finally:
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")
try:
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")
finally:
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()

cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec")
try:
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")
finally:
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")
try:
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
finally:
cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null")


def test_cursor_setinputsizes_reset(db_connection):
"""Test that setinputsizes is reset after execution"""

Expand Down
Loading