Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 21 additions & 2 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,
Comment thread
jahnvi480 marked this conversation as resolved.
Outdated
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,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:
Comment thread
jahnvi480 marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -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,
Comment thread
jahnvi480 marked this conversation as resolved.
Expand Down
125 changes: 125 additions & 0 deletions tests/test_004_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Comment thread
jahnvi480 marked this conversation as resolved.
Outdated
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")


Comment thread
jahnvi480 marked this conversation as resolved.
def test_cursor_setinputsizes_reset(db_connection):
"""Test that setinputsizes is reset after execution"""

Expand Down
Loading