diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 6365d559..f387e055 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1712,14 +1712,23 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: for row in seq_of_parameters: processed_row = list(row) for i, val in enumerate(processed_row): - if (parameters_type[i].paramSQLType in + if val is None: + continue + # Convert Decimals for money/smallmoney to string + if isinstance(val, decimal.Decimal) and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value: + processed_row[i] = str(val) + # Existing numeric conversion + elif (parameters_type[i].paramSQLType in (ddbc_sql_const.SQL_DECIMAL.value, ddbc_sql_const.SQL_NUMERIC.value) and - not isinstance(val, decimal.Decimal) and val is not None): + not isinstance(val, decimal.Decimal)): try: processed_row[i] = decimal.Decimal(str(val)) - except: - pass # Keep original value if conversion fails + except Exception as e: + raise ValueError( + f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}" + ) processed_parameters.append(processed_row) + # Now transpose the processed parameters columnwise_params, row_count = self._transpose_rowwise_to_columnwise(processed_parameters) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 0b53d449..07b75497 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6729,9 +6729,9 @@ def test_nvarcharmax_large(cursor, db_connection): def test_money_smallmoney_insert_fetch(cursor, db_connection): """Test inserting and retrieving valid MONEY and SMALLMONEY values including boundaries and typical data""" try: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") cursor.execute(""" - CREATE TABLE dbo.money_test ( + CREATE TABLE #pytest_money_test ( id INT IDENTITY PRIMARY KEY, m MONEY, sm SMALLMONEY, @@ -6742,27 +6742,27 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection): db_connection.commit() # Max values - cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"), decimal.Decimal("9999999999999.9999"), decimal.Decimal("1234.5678"))) # Min values - cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"), decimal.Decimal("-9999999999999.9999"), decimal.Decimal("-1234.5678"))) # Typical values - cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", (decimal.Decimal("1234567.8901"), decimal.Decimal("12345.6789"), decimal.Decimal("42.4242"), decimal.Decimal("3.1415"))) # NULL values - cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", (None, None, None, None)) db_connection.commit() - cursor.execute("SELECT m, sm, d, n FROM dbo.money_test ORDER BY id") + cursor.execute("SELECT m, sm, d, n FROM #pytest_money_test ORDER BY id") results = cursor.fetchall() assert len(results) == 4, f"Expected 4 rows, got {len(results)}" @@ -6787,16 +6787,15 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection): except Exception as e: pytest.fail(f"MONEY and SMALLMONEY insert/fetch test failed: {e}") finally: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") db_connection.commit() def test_money_smallmoney_null_handling(cursor, db_connection): """Test that NULL values for MONEY and SMALLMONEY are stored and retrieved correctly""" try: - drop_table_if_exists(cursor, "dbo.money_test") cursor.execute(""" - CREATE TABLE dbo.money_test ( + CREATE TABLE #pytest_money_test ( id INT IDENTITY PRIMARY KEY, m MONEY, sm SMALLMONEY @@ -6805,19 +6804,19 @@ def test_money_smallmoney_null_handling(cursor, db_connection): db_connection.commit() # Row with both NULLs - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", (None, None)) + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (None, None)) # Row with m filled, sm NULL - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (decimal.Decimal("123.4500"), None)) # Row with m NULL, sm filled - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (None, decimal.Decimal("67.8900"))) db_connection.commit() - cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id") + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id") results = cursor.fetchall() assert len(results) == 3, f"Expected 3 rows, got {len(results)}" @@ -6838,16 +6837,15 @@ def test_money_smallmoney_null_handling(cursor, db_connection): except Exception as e: pytest.fail(f"MONEY and SMALLMONEY NULL handling test failed: {e}") finally: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") db_connection.commit() def test_money_smallmoney_roundtrip(cursor, db_connection): """Test inserting and retrieving MONEY and SMALLMONEY using decimal.Decimal roundtrip""" try: - drop_table_if_exists(cursor, "dbo.money_test") cursor.execute(""" - CREATE TABLE dbo.money_test ( + CREATE TABLE #pytest_money_test ( id INT IDENTITY PRIMARY KEY, m MONEY, sm SMALLMONEY @@ -6856,10 +6854,10 @@ def test_money_smallmoney_roundtrip(cursor, db_connection): db_connection.commit() values = (decimal.Decimal("12345.6789"), decimal.Decimal("987.6543")) - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", values) + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", values) db_connection.commit() - cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC") + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id DESC") row = cursor.fetchone() for i, (val, exp_val) in enumerate(zip(row, values), 1): assert val == exp_val, f"col{i} roundtrip mismatch, got {val}, expected {exp_val}" @@ -6868,16 +6866,16 @@ def test_money_smallmoney_roundtrip(cursor, db_connection): except Exception as e: pytest.fail(f"MONEY and SMALLMONEY roundtrip test failed: {e}") finally: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") db_connection.commit() def test_money_smallmoney_boundaries(cursor, db_connection): """Test boundary values for MONEY and SMALLMONEY types are handled correctly""" try: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") cursor.execute(""" - CREATE TABLE dbo.money_test ( + CREATE TABLE #pytest_money_test ( id INT IDENTITY PRIMARY KEY, m MONEY, sm SMALLMONEY @@ -6886,16 +6884,16 @@ def test_money_smallmoney_boundaries(cursor, db_connection): db_connection.commit() # Insert max boundary - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"))) # Insert min boundary - cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"))) db_connection.commit() - cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC") + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id DESC") results = cursor.fetchall() expected = [ (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648")), @@ -6909,16 +6907,15 @@ def test_money_smallmoney_boundaries(cursor, db_connection): except Exception as e: pytest.fail(f"MONEY and SMALLMONEY boundary values test failed: {e}") finally: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") db_connection.commit() def test_money_smallmoney_invalid_values(cursor, db_connection): """Test that invalid or out-of-range MONEY and SMALLMONEY values raise errors""" try: - drop_table_if_exists(cursor, "dbo.money_test") cursor.execute(""" - CREATE TABLE dbo.money_test ( + CREATE TABLE #pytest_money_test ( id INT IDENTITY PRIMARY KEY, m MONEY, sm SMALLMONEY @@ -6928,20 +6925,123 @@ def test_money_smallmoney_invalid_values(cursor, db_connection): # Out of range MONEY with pytest.raises(Exception): - cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", (decimal.Decimal("922337203685477.5808"),)) + cursor.execute("INSERT INTO #pytest_money_test (m) VALUES (?)", (decimal.Decimal("922337203685477.5808"),)) # Out of range SMALLMONEY with pytest.raises(Exception): - cursor.execute("INSERT INTO dbo.money_test (sm) VALUES (?)", (decimal.Decimal("214748.3648"),)) + cursor.execute("INSERT INTO #pytest_money_test (sm) VALUES (?)", (decimal.Decimal("214748.3648"),)) # Invalid string with pytest.raises(Exception): - cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", ("invalid_string",)) + cursor.execute("INSERT INTO #pytest_money_test (m) VALUES (?)", ("invalid_string",)) except Exception as e: pytest.fail(f"MONEY and SMALLMONEY invalid values test failed: {e}") finally: - drop_table_if_exists(cursor, "dbo.money_test") + drop_table_if_exists(cursor, "#pytest_money_test") + db_connection.commit() + +def test_money_smallmoney_roundtrip_executemany(cursor, db_connection): + """Test inserting and retrieving MONEY and SMALLMONEY using executemany with decimal.Decimal""" + try: + cursor.execute(""" + CREATE TABLE #pytest_money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + test_data = [ + (decimal.Decimal("12345.6789"), decimal.Decimal("987.6543")), + (decimal.Decimal("0.0001"), decimal.Decimal("0.01")), + (None, decimal.Decimal("42.42")), + (decimal.Decimal("-1000.99"), None), + ] + + # Insert using executemany directly with Decimals + cursor.executemany( + "INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", + test_data + ) + db_connection.commit() + + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id") + results = cursor.fetchall() + assert len(results) == len(test_data) + + for i, (row, expected) in enumerate(zip(results, test_data), 1): + for j, (val, exp_val) in enumerate(zip(row, expected), 1): + if exp_val is None: + assert val is None + else: + assert val == exp_val + assert isinstance(val, decimal.Decimal) + + finally: + drop_table_if_exists(cursor, "#pytest_money_test") + db_connection.commit() + + +def test_money_smallmoney_executemany_null_handling(cursor, db_connection): + """Test inserting NULLs into MONEY and SMALLMONEY using executemany""" + try: + cursor.execute(""" + CREATE TABLE #pytest_money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + rows = [ + (None, None), + (decimal.Decimal("123.4500"), None), + (None, decimal.Decimal("67.8900")), + ] + cursor.executemany("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", rows) + db_connection.commit() + + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id ASC") + results = cursor.fetchall() + assert len(results) == len(rows) + + for row, expected in zip(results, rows): + for val, exp_val in zip(row, expected): + if exp_val is None: + assert val is None + else: + assert val == exp_val + assert isinstance(val, decimal.Decimal) + + finally: + drop_table_if_exists(cursor, "#pytest_money_test") + db_connection.commit() + +def test_money_smallmoney_out_of_range_low(cursor, db_connection): + """Test inserting values just below the minimum MONEY/SMALLMONEY range raises error""" + try: + drop_table_if_exists(cursor, "#pytest_money_test") + cursor.execute("CREATE TABLE #pytest_money_test (m MONEY, sm SMALLMONEY)") + db_connection.commit() + + # Just below minimum MONEY + with pytest.raises(Exception): + cursor.execute( + "INSERT INTO #pytest_money_test (m) VALUES (?)", + (decimal.Decimal("-922337203685477.5809"),) + ) + + # Just below minimum SMALLMONEY + with pytest.raises(Exception): + cursor.execute( + "INSERT INTO #pytest_money_test (sm) VALUES (?)", + (decimal.Decimal("-214748.3649"),) + ) + finally: + drop_table_if_exists(cursor, "#pytest_money_test") db_connection.commit() def test_uuid_insert_and_select_none(cursor, db_connection):