Skip to content

Commit 802b65d

Browse files
committed
FIX: Setinputsizes() SQL_DECIMAL crash
1 parent 9688b10 commit 802b65d

2 files changed

Lines changed: 146 additions & 2 deletions

File tree

mssql_python/cursor.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,8 +885,8 @@ def _get_c_type_for_sql_type(self, sql_type: int) -> int:
885885
ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
886886
ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
887887
ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
888-
ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value,
889-
ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value,
888+
ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_CHAR.value,
889+
ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_CHAR.value,
890890
ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value,
891891
ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value,
892892
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
949949
# For non-NULL parameters, determine the appropriate C type based on SQL type
950950
c_type = self._get_c_type_for_sql_type(sql_type)
951951

952+
# Convert Decimal to string for SQL_C_CHAR binding (GH-503)
953+
if (
954+
isinstance(parameter, decimal.Decimal)
955+
and sql_type in (
956+
ddbc_sql_const.SQL_DECIMAL.value,
957+
ddbc_sql_const.SQL_NUMERIC.value,
958+
)
959+
):
960+
parameters_list[i] = format(parameter, "f")
961+
952962
# Check if this should be a DAE (data at execution) parameter
953963
# For string types with large column sizes
954964
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
23062316
and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value
23072317
):
23082318
processed_row[i] = format(val, "f")
2319+
# Convert Decimal to string for SQL_C_CHAR binding (GH-503)
2320+
elif (
2321+
isinstance(val, decimal.Decimal)
2322+
and parameters_type[i].paramSQLType in (
2323+
ddbc_sql_const.SQL_DECIMAL.value,
2324+
ddbc_sql_const.SQL_NUMERIC.value,
2325+
)
2326+
):
2327+
processed_row[i] = format(val, "f")
23092328
# Existing numeric conversion
23102329
elif parameters_type[i].paramSQLType in (
23112330
ddbc_sql_const.SQL_DECIMAL.value,

tests/test_004_cursor.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9974,6 +9974,131 @@ def test_cursor_setinputsizes_with_executemany_float(db_connection):
99749974
cursor.execute("DROP TABLE IF EXISTS #test_inputsizes_float")
99759975

99769976

9977+
def test_setinputsizes_sql_decimal_with_executemany(db_connection):
9978+
"""Test setinputsizes with SQL_DECIMAL accepts Python Decimal values (GH-503).
9979+
9980+
Without this fix, passing SQL_DECIMAL or SQL_NUMERIC via setinputsizes()
9981+
caused a RuntimeError because Decimal objects were not converted to
9982+
NumericData before the C binding validated the C type.
9983+
"""
9984+
cursor = db_connection.cursor()
9985+
9986+
cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal")
9987+
cursor.execute("""
9988+
CREATE TABLE #test_sis_decimal (
9989+
Name NVARCHAR(100),
9990+
CategoryID INT,
9991+
Price DECIMAL(18,2)
9992+
)
9993+
""")
9994+
9995+
cursor.setinputsizes([
9996+
(mssql_python.SQL_WVARCHAR, 100, 0),
9997+
(mssql_python.SQL_INTEGER, 0, 0),
9998+
(mssql_python.SQL_DECIMAL, 18, 2),
9999+
])
10000+
10001+
cursor.executemany(
10002+
"INSERT INTO #test_sis_decimal (Name, CategoryID, Price) VALUES (?, ?, ?)",
10003+
[
10004+
("Widget", 1, decimal.Decimal("19.99")),
10005+
("Gadget", 2, decimal.Decimal("29.99")),
10006+
("Gizmo", 3, decimal.Decimal("0.01")),
10007+
],
10008+
)
10009+
10010+
cursor.execute("SELECT Name, CategoryID, Price FROM #test_sis_decimal ORDER BY CategoryID")
10011+
rows = cursor.fetchall()
10012+
10013+
assert len(rows) == 3
10014+
assert rows[0][0] == "Widget"
10015+
assert rows[0][1] == 1
10016+
assert rows[0][2] == decimal.Decimal("19.99")
10017+
assert rows[1][0] == "Gadget"
10018+
assert rows[1][1] == 2
10019+
assert rows[1][2] == decimal.Decimal("29.99")
10020+
assert rows[2][0] == "Gizmo"
10021+
assert rows[2][1] == 3
10022+
assert rows[2][2] == decimal.Decimal("0.01")
10023+
10024+
cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal")
10025+
10026+
10027+
def test_setinputsizes_sql_numeric_with_executemany(db_connection):
10028+
"""Test setinputsizes with SQL_NUMERIC accepts Python Decimal values (GH-503)."""
10029+
cursor = db_connection.cursor()
10030+
10031+
cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric")
10032+
cursor.execute("""
10033+
CREATE TABLE #test_sis_numeric (
10034+
Value NUMERIC(10,4)
10035+
)
10036+
""")
10037+
10038+
cursor.setinputsizes([
10039+
(mssql_python.SQL_NUMERIC, 10, 4),
10040+
])
10041+
10042+
cursor.executemany(
10043+
"INSERT INTO #test_sis_numeric (Value) VALUES (?)",
10044+
[
10045+
(decimal.Decimal("123.4567"),),
10046+
(decimal.Decimal("-99.0001"),),
10047+
(decimal.Decimal("0.0000"),),
10048+
],
10049+
)
10050+
10051+
cursor.execute("SELECT Value FROM #test_sis_numeric ORDER BY Value")
10052+
rows = cursor.fetchall()
10053+
10054+
assert len(rows) == 3
10055+
assert rows[0][0] == decimal.Decimal("-99.0001")
10056+
assert rows[1][0] == decimal.Decimal("0.0000")
10057+
assert rows[2][0] == decimal.Decimal("123.4567")
10058+
10059+
cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric")
10060+
10061+
10062+
def test_setinputsizes_sql_decimal_with_execute(db_connection):
10063+
"""Test setinputsizes with SQL_DECIMAL works with single execute() too (GH-503)."""
10064+
cursor = db_connection.cursor()
10065+
10066+
cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec")
10067+
cursor.execute("CREATE TABLE #test_sis_dec_exec (Price DECIMAL(18,2))")
10068+
10069+
cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10070+
cursor.execute(
10071+
"INSERT INTO #test_sis_dec_exec (Price) VALUES (?)",
10072+
decimal.Decimal("99.95"),
10073+
)
10074+
10075+
cursor.execute("SELECT Price FROM #test_sis_dec_exec")
10076+
row = cursor.fetchone()
10077+
assert row[0] == decimal.Decimal("99.95")
10078+
10079+
cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec")
10080+
10081+
10082+
def test_setinputsizes_sql_decimal_null(db_connection):
10083+
"""Test setinputsizes with SQL_DECIMAL handles NULL values correctly (GH-503)."""
10084+
cursor = db_connection.cursor()
10085+
10086+
cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null")
10087+
cursor.execute("CREATE TABLE #test_sis_dec_null (Price DECIMAL(18,2))")
10088+
10089+
cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10090+
cursor.execute(
10091+
"INSERT INTO #test_sis_dec_null (Price) VALUES (?)",
10092+
None,
10093+
)
10094+
10095+
cursor.execute("SELECT Price FROM #test_sis_dec_null")
10096+
row = cursor.fetchone()
10097+
assert row[0] is None
10098+
10099+
cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null")
10100+
10101+
997710102
def test_cursor_setinputsizes_reset(db_connection):
997810103
"""Test that setinputsizes is reset after execution"""
997910104

0 commit comments

Comments
 (0)