From 511e0cea9a87731e42d23e7152f2872377ddc7fe Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 14:48:30 +0530 Subject: [PATCH 1/4] money-executemany --- mssql_python/cursor.py | 24 ++++++++++-- tests/test_004_cursor.py | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index b6b309cf..c6814b8e 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1674,17 +1674,35 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: # Process parameters into column-wise format with possible type conversions # First, convert any Decimal types as needed for NUMERIC/DECIMAL columns processed_parameters = [] + # for row in seq_of_parameters: + # processed_row = list(row) + # for i, val in enumerate(processed_row): + # if (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): + # try: + # processed_row[i] = decimal.Decimal(str(val)) + # except: + # pass # Keep original value if conversion fails + # processed_parameters.append(processed_row) 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 + pass 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 9b7276ab..7e9ff88c 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6942,6 +6942,87 @@ def test_money_smallmoney_invalid_values(cursor, db_connection): drop_table_if_exists(cursor, "dbo.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: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.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 dbo.money_test (m, sm) VALUES (?, ?)", + test_data + ) + db_connection.commit() + + cursor.execute("SELECT m, sm FROM dbo.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, "dbo.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: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.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 dbo.money_test (m, sm) VALUES (?, ?)", rows) + db_connection.commit() + + cursor.execute("SELECT m, sm FROM dbo.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, "dbo.money_test") + db_connection.commit() + def test_decimal_separator_with_multiple_values(cursor, db_connection): """Test decimal separator with multiple different decimal values""" original_separator = mssql_python.getDecimalSeparator() From 890b1c56b3f7356320c6a27ffc6f6df8f22f0be2 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 14:51:37 +0530 Subject: [PATCH 2/4] cleanup --- mssql_python/cursor.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index c6814b8e..b3426422 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1674,17 +1674,6 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: # Process parameters into column-wise format with possible type conversions # First, convert any Decimal types as needed for NUMERIC/DECIMAL columns processed_parameters = [] - # for row in seq_of_parameters: - # processed_row = list(row) - # for i, val in enumerate(processed_row): - # if (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): - # try: - # processed_row[i] = decimal.Decimal(str(val)) - # except: - # pass # Keep original value if conversion fails - # processed_parameters.append(processed_row) for row in seq_of_parameters: processed_row = list(row) for i, val in enumerate(processed_row): From a35f61bbe78ca1cba0b72eb895bacd03e3bc39cd Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 24 Sep 2025 08:31:57 +0530 Subject: [PATCH 3/4] minor --- mssql_python/cursor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index b3426422..2205dbd2 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1688,8 +1688,10 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: not isinstance(val, decimal.Decimal)): try: processed_row[i] = decimal.Decimal(str(val)) - except: - pass + except Exception as e: + raise ValueError( + f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}" + ) processed_parameters.append(processed_row) From 93526b967a86dd238013120f5eaf733efadf2ffa Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 24 Sep 2025 10:40:16 +0530 Subject: [PATCH 4/4] resolving comments --- tests/test_004_cursor.py | 103 +++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 7e9ff88c..cc322e09 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6727,9 +6727,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, @@ -6740,27 +6740,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)}" @@ -6785,16 +6785,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 @@ -6803,19 +6802,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)}" @@ -6836,16 +6835,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 @@ -6854,10 +6852,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}" @@ -6866,16 +6864,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 @@ -6884,16 +6882,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")), @@ -6907,16 +6905,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 @@ -6926,28 +6923,27 @@ 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: - 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 @@ -6964,12 +6960,12 @@ def test_money_smallmoney_roundtrip_executemany(cursor, db_connection): # Insert using executemany directly with Decimals cursor.executemany( - "INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + "INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", test_data ) 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) == len(test_data) @@ -6982,16 +6978,15 @@ def test_money_smallmoney_roundtrip_executemany(cursor, db_connection): assert isinstance(val, decimal.Decimal) finally: - drop_table_if_exists(cursor, "dbo.money_test") + 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: - 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 @@ -7004,10 +6999,10 @@ def test_money_smallmoney_executemany_null_handling(cursor, db_connection): (decimal.Decimal("123.4500"), None), (None, decimal.Decimal("67.8900")), ] - cursor.executemany("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", rows) + cursor.executemany("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", rows) db_connection.commit() - cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id ASC") + cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id ASC") results = cursor.fetchall() assert len(results) == len(rows) @@ -7020,7 +7015,31 @@ def test_money_smallmoney_executemany_null_handling(cursor, db_connection): assert isinstance(val, decimal.Decimal) finally: - drop_table_if_exists(cursor, "dbo.money_test") + 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_decimal_separator_with_multiple_values(cursor, db_connection):