@@ -9974,6 +9974,298 @@ 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+ try:
9988+ cursor.execute("""
9989+ CREATE TABLE #test_sis_decimal (
9990+ Name NVARCHAR(100),
9991+ CategoryID INT,
9992+ Price DECIMAL(18,2)
9993+ )
9994+ """)
9995+
9996+ cursor.setinputsizes(
9997+ [
9998+ (mssql_python.SQL_WVARCHAR, 100, 0),
9999+ (mssql_python.SQL_INTEGER, 0, 0),
10000+ (mssql_python.SQL_DECIMAL, 18, 2),
10001+ ]
10002+ )
10003+
10004+ cursor.executemany(
10005+ "INSERT INTO #test_sis_decimal (Name, CategoryID, Price) VALUES (?, ?, ?)",
10006+ [
10007+ ("Widget", 1, decimal.Decimal("19.99")),
10008+ ("Gadget", 2, decimal.Decimal("29.99")),
10009+ ("Gizmo", 3, decimal.Decimal("0.01")),
10010+ ],
10011+ )
10012+
10013+ cursor.execute("SELECT Name, CategoryID, Price FROM #test_sis_decimal ORDER BY CategoryID")
10014+ rows = cursor.fetchall()
10015+
10016+ assert len(rows) == 3
10017+ assert rows[0][0] == "Widget"
10018+ assert rows[0][1] == 1
10019+ assert rows[0][2] == decimal.Decimal("19.99")
10020+ assert rows[1][0] == "Gadget"
10021+ assert rows[1][1] == 2
10022+ assert rows[1][2] == decimal.Decimal("29.99")
10023+ assert rows[2][0] == "Gizmo"
10024+ assert rows[2][1] == 3
10025+ assert rows[2][2] == decimal.Decimal("0.01")
10026+ finally:
10027+ cursor.execute("DROP TABLE IF EXISTS #test_sis_decimal")
10028+
10029+
10030+ def test_setinputsizes_sql_numeric_with_executemany(db_connection):
10031+ """Test setinputsizes with SQL_NUMERIC accepts Python Decimal values (GH-503)."""
10032+ cursor = db_connection.cursor()
10033+
10034+ cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric")
10035+ try:
10036+ cursor.execute("""
10037+ CREATE TABLE #test_sis_numeric (
10038+ Value NUMERIC(10,4)
10039+ )
10040+ """)
10041+
10042+ cursor.setinputsizes(
10043+ [
10044+ (mssql_python.SQL_NUMERIC, 10, 4),
10045+ ]
10046+ )
10047+
10048+ cursor.executemany(
10049+ "INSERT INTO #test_sis_numeric (Value) VALUES (?)",
10050+ [
10051+ (decimal.Decimal("123.4567"),),
10052+ (decimal.Decimal("-99.0001"),),
10053+ (decimal.Decimal("0.0000"),),
10054+ ],
10055+ )
10056+
10057+ cursor.execute("SELECT Value FROM #test_sis_numeric ORDER BY Value")
10058+ rows = cursor.fetchall()
10059+
10060+ assert len(rows) == 3
10061+ assert rows[0][0] == decimal.Decimal("-99.0001")
10062+ assert rows[1][0] == decimal.Decimal("0.0000")
10063+ assert rows[2][0] == decimal.Decimal("123.4567")
10064+ finally:
10065+ cursor.execute("DROP TABLE IF EXISTS #test_sis_numeric")
10066+
10067+
10068+ def test_setinputsizes_sql_decimal_with_non_decimal_values(db_connection):
10069+ """Test setinputsizes with SQL_DECIMAL converts non-Decimal values (int/float) to string (GH-503)."""
10070+ cursor = db_connection.cursor()
10071+
10072+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_nondec")
10073+ try:
10074+ cursor.execute("CREATE TABLE #test_sis_dec_nondc (Price DECIMAL(18,2))")
10075+
10076+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10077+
10078+ # Pass int and float instead of Decimal — exercises the non-Decimal conversion branch
10079+ cursor.executemany(
10080+ "INSERT INTO #test_sis_dec_nondc (Price) VALUES (?)",
10081+ [(42,), (19.99,), (0,)],
10082+ )
10083+
10084+ cursor.execute("SELECT Price FROM #test_sis_dec_nondc ORDER BY Price")
10085+ rows = cursor.fetchall()
10086+
10087+ assert len(rows) == 3
10088+ assert rows[0][0] == decimal.Decimal("0.00")
10089+ assert rows[1][0] == decimal.Decimal("19.99")
10090+ assert rows[2][0] == decimal.Decimal("42.00")
10091+ finally:
10092+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_nondc")
10093+
10094+
10095+ def test_setinputsizes_sql_decimal_with_execute(db_connection):
10096+ """Test setinputsizes with SQL_DECIMAL works with single execute() too (GH-503)."""
10097+ cursor = db_connection.cursor()
10098+
10099+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec")
10100+ try:
10101+ cursor.execute("CREATE TABLE #test_sis_dec_exec (Price DECIMAL(18,2))")
10102+
10103+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10104+ cursor.execute(
10105+ "INSERT INTO #test_sis_dec_exec (Price) VALUES (?)",
10106+ decimal.Decimal("99.95"),
10107+ )
10108+
10109+ cursor.execute("SELECT Price FROM #test_sis_dec_exec")
10110+ row = cursor.fetchone()
10111+ assert row[0] == decimal.Decimal("99.95")
10112+ finally:
10113+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_exec")
10114+
10115+
10116+ def test_setinputsizes_sql_decimal_null(db_connection):
10117+ """Test setinputsizes with SQL_DECIMAL handles NULL values correctly (GH-503)."""
10118+ cursor = db_connection.cursor()
10119+
10120+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null")
10121+ try:
10122+ cursor.execute("CREATE TABLE #test_sis_dec_null (Price DECIMAL(18,2))")
10123+
10124+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10125+ cursor.execute(
10126+ "INSERT INTO #test_sis_dec_null (Price) VALUES (?)",
10127+ None,
10128+ )
10129+
10130+ cursor.execute("SELECT Price FROM #test_sis_dec_null")
10131+ row = cursor.fetchone()
10132+ assert row[0] is None
10133+ finally:
10134+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_null")
10135+
10136+
10137+ def test_setinputsizes_sql_decimal_unconvertible_value(db_connection):
10138+ """Test setinputsizes with SQL_DECIMAL raises ValueError for unconvertible values (GH-503)."""
10139+ cursor = db_connection.cursor()
10140+
10141+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_bad")
10142+ try:
10143+ cursor.execute("CREATE TABLE #test_sis_dec_bad (Price DECIMAL(18,2))")
10144+
10145+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10146+
10147+ with pytest.raises(ValueError, match="Failed to convert parameter"):
10148+ cursor.executemany(
10149+ "INSERT INTO #test_sis_dec_bad (Price) VALUES (?)",
10150+ [("not_a_number",)],
10151+ )
10152+ finally:
10153+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_bad")
10154+
10155+
10156+ def test_setinputsizes_sql_decimal_high_precision(db_connection):
10157+ """Test setinputsizes with SQL_DECIMAL preserves full DECIMAL(38,18) precision (GH-503)."""
10158+ cursor = db_connection.cursor()
10159+
10160+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_hp")
10161+ try:
10162+ cursor.execute("CREATE TABLE #test_sis_dec_hp (Value DECIMAL(38,18))")
10163+
10164+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 38, 18)])
10165+
10166+ high_prec = decimal.Decimal("12345678901234567890.123456789012345678")
10167+ cursor.execute(
10168+ "INSERT INTO #test_sis_dec_hp (Value) VALUES (?)",
10169+ high_prec,
10170+ )
10171+
10172+ cursor.execute("SELECT Value FROM #test_sis_dec_hp")
10173+ row = cursor.fetchone()
10174+ assert row[0] == high_prec
10175+ finally:
10176+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_hp")
10177+
10178+
10179+ def test_setinputsizes_sql_decimal_negative_zero(db_connection):
10180+ """Test setinputsizes with SQL_DECIMAL handles Decimal('-0.00') correctly (GH-503)."""
10181+ cursor = db_connection.cursor()
10182+
10183+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_negz")
10184+ try:
10185+ cursor.execute("CREATE TABLE #test_sis_dec_negz (Value DECIMAL(18,2))")
10186+
10187+ cursor.setinputsizes([(mssql_python.SQL_DECIMAL, 18, 2)])
10188+ cursor.execute(
10189+ "INSERT INTO #test_sis_dec_negz (Value) VALUES (?)",
10190+ decimal.Decimal("-0.00"),
10191+ )
10192+
10193+ cursor.execute("SELECT Value FROM #test_sis_dec_negz")
10194+ row = cursor.fetchone()
10195+ # SQL Server normalizes -0.00 to 0.00
10196+ assert row[0] == decimal.Decimal("0.00")
10197+ finally:
10198+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_negz")
10199+
10200+
10201+ def test_setinputsizes_sql_decimal_mixed_null_executemany(db_connection):
10202+ """Test setinputsizes with SQL_DECIMAL handles mixed NULL/non-NULL in executemany (GH-503)."""
10203+ cursor = db_connection.cursor()
10204+
10205+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_mix")
10206+ try:
10207+ cursor.execute("CREATE TABLE #test_sis_dec_mix (Id INT, Price DECIMAL(18,2))")
10208+
10209+ cursor.setinputsizes(
10210+ [
10211+ (mssql_python.SQL_INTEGER, 0, 0),
10212+ (mssql_python.SQL_DECIMAL, 18, 2),
10213+ ]
10214+ )
10215+
10216+ cursor.executemany(
10217+ "INSERT INTO #test_sis_dec_mix (Id, Price) VALUES (?, ?)",
10218+ [
10219+ (1, decimal.Decimal("10.50")),
10220+ (2, None),
10221+ (3, decimal.Decimal("30.75")),
10222+ (4, None),
10223+ ],
10224+ )
10225+
10226+ cursor.execute("SELECT Id, Price FROM #test_sis_dec_mix ORDER BY Id")
10227+ rows = cursor.fetchall()
10228+
10229+ assert len(rows) == 4
10230+ assert rows[0][1] == decimal.Decimal("10.50")
10231+ assert rows[1][1] is None
10232+ assert rows[2][1] == decimal.Decimal("30.75")
10233+ assert rows[3][1] is None
10234+ finally:
10235+ cursor.execute("DROP TABLE IF EXISTS #test_sis_dec_mix")
10236+
10237+
10238+ def test_decimal_without_setinputsizes_no_regression(db_connection):
10239+ """Verify plain Decimal binding without setinputsizes still works (GH-503 regression check)."""
10240+ cursor = db_connection.cursor()
10241+
10242+ cursor.execute("DROP TABLE IF EXISTS #test_dec_noreg")
10243+ try:
10244+ cursor.execute("CREATE TABLE #test_dec_noreg (Price DECIMAL(18,2))")
10245+
10246+ # Single execute without setinputsizes
10247+ cursor.execute(
10248+ "INSERT INTO #test_dec_noreg (Price) VALUES (?)",
10249+ decimal.Decimal("49.99"),
10250+ )
10251+
10252+ # executemany without setinputsizes
10253+ cursor.executemany(
10254+ "INSERT INTO #test_dec_noreg (Price) VALUES (?)",
10255+ [(decimal.Decimal("99.99"),), (decimal.Decimal("0.01"),)],
10256+ )
10257+
10258+ cursor.execute("SELECT Price FROM #test_dec_noreg ORDER BY Price")
10259+ rows = cursor.fetchall()
10260+
10261+ assert len(rows) == 3
10262+ assert rows[0][0] == decimal.Decimal("0.01")
10263+ assert rows[1][0] == decimal.Decimal("49.99")
10264+ assert rows[2][0] == decimal.Decimal("99.99")
10265+ finally:
10266+ cursor.execute("DROP TABLE IF EXISTS #test_dec_noreg")
10267+
10268+
997710269def test_cursor_setinputsizes_reset(db_connection):
997810270 """Test that setinputsizes is reset after execution"""
997910271
0 commit comments