From 61290d58781fbd066230fc33052b66941552ee79 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Thu, 30 Oct 2025 12:51:07 +0530 Subject: [PATCH 1/3] fixed --- mssql_python/cursor.py | 9 ++-- tests/test_004_cursor.py | 101 ++++++++++++++++++++++++++------------- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 446a2dfb..adfc5042 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -399,8 +399,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg # Detect MONEY / SMALLMONEY range if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX: - # smallmoney - parameters_list[i] = str(param) + parameters_list[i] = format(param, 'f') return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, @@ -409,8 +408,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg False, ) if MONEY_MIN <= param <= MONEY_MAX: - # money - parameters_list[i] = str(param) + parameters_list[i] = format(param, 'f') return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, @@ -1876,13 +1874,12 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s for i, val in enumerate(processed_row): 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) + processed_row[i] = format(val, 'f') # Existing numeric conversion elif parameters_type[i].paramSQLType in ( ddbc_sql_const.SQL_DECIMAL.value, diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index b52b0656..a7f2c8ae 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -13941,20 +13941,6 @@ def test_numeric_leading_zeros_precision_loss( actual == expected ), f"Leading zeros precision loss for {value}, expected {expected}, got {actual}" - except Exception as e: - # Handle cases where values get converted to scientific notation and cause SQL Server conversion errors - error_msg = str(e).lower() - if ( - "converting" in error_msg - and "varchar" in error_msg - and "numeric" in error_msg - ): - pytest.skip( - f"Value {value} converted to scientific notation, causing expected SQL Server conversion error: {e}" - ) - else: - raise # Re-raise unexpected errors - finally: try: cursor.execute(f"DROP TABLE {table_name}") @@ -14002,24 +13988,6 @@ def test_numeric_extreme_exponents_precision_loss( "1E-18" ), f"Extreme exponent value not preserved for {description}: {value} -> {actual}" - except Exception as e: - # Handle expected SQL Server validation errors for scientific notation values - error_msg = str(e).lower() - if "scale" in error_msg and "range" in error_msg: - # This is expected - SQL Server rejects invalid scale/precision combinations - pytest.skip( - f"Expected SQL Server scale/precision validation for {description}: {e}" - ) - elif any( - keyword in error_msg - for keyword in ["converting", "overflow", "precision", "varchar", "numeric"] - ): - # Other expected precision/conversion issues - pytest.skip( - f"Expected SQL Server precision limits or VARCHAR conversion for {description}: {e}" - ) - else: - raise # Re-raise if it's not a precision-related error finally: try: cursor.execute(f"DROP TABLE {table_name}") @@ -14027,7 +13995,6 @@ def test_numeric_extreme_exponents_precision_loss( except: pass # Table might not exist if creation failed - # --------------------------------------------------------- # Test 12: 38-digit precision boundary limits # --------------------------------------------------------- @@ -14123,6 +14090,72 @@ def test_numeric_beyond_38_digit_precision_negative( ), f"Expected SQL Server precision limit message for {description}, got: {error_msg}" +@pytest.mark.parametrize( + "values, description", + [ + # Small decimal values with scientific notation + ( + [ + decimal.Decimal('0.70000000000696'), + decimal.Decimal('1E-7'), + decimal.Decimal('0.00001'), + decimal.Decimal('6.96E-12'), + ], + "Small decimals with scientific notation" + ), + # Large decimal values with scientific notation + ( + [ + decimal.Decimal('4E+8'), + decimal.Decimal('1.521E+15'), + decimal.Decimal('5.748E+18'), + decimal.Decimal('1E+11') + ], + "Large decimals with positive exponents" + ), + # Medium-sized decimals + ( + [ + decimal.Decimal('123.456'), + decimal.Decimal('9999.9999'), + decimal.Decimal('1000000.50') + ], + "Medium-sized decimals" + ), + ], +) +def test_decimal_scientific_notation_to_varchar(cursor, db_connection, values, description): + """ + Test that Decimal values with scientific notation are properly converted + to VARCHAR without triggering 'varchar to numeric' conversion errors. + This verifies that the driver correctly handles Decimal to VARCHAR conversion + """ + table_name = "#pytest_decimal_varchar_conversion" + try: + cursor.execute(f"CREATE TABLE {table_name} (id INT IDENTITY(1,1), val VARCHAR(50))") + + for val in values: + cursor.execute(f"INSERT INTO {table_name} (val) VALUES (?)", (val,)) + db_connection.commit() + + cursor.execute(f"SELECT val FROM {table_name} ORDER BY id") + rows = cursor.fetchall() + + assert len(rows) == len(values), f"Expected {len(values)} rows, got {len(rows)}" + + for i, (row, expected_val) in enumerate(zip(rows, values)): + stored_val = decimal.Decimal(row[0]) + assert stored_val == expected_val, ( + f"{description}: Row {i} mismatch - expected {expected_val}, got {stored_val}" + ) + + finally: + try: + cursor.execute(f"DROP TABLE {table_name}") + db_connection.commit() + except: + pass + SMALL_XML = "1" LARGE_XML = "" + "".join(f"{i}" for i in range(10000)) + "" EMPTY_XML = "" @@ -15038,4 +15071,4 @@ def test_close(db_connection): except Exception as e: pytest.fail(f"Cursor close test failed: {e}") finally: - cursor = db_connection.cursor() + cursor = db_connection.cursor() \ No newline at end of file From 00b7bd03343d0d4fb5e65593c598d8595459549b Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 3 Nov 2025 22:43:37 +0530 Subject: [PATCH 2/3] addressed review comment --- mssql_python/cursor.py | 8 +++-- tests/test_004_cursor.py | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index adfc5042..db1d14bd 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -399,7 +399,9 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg # Detect MONEY / SMALLMONEY range if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX: - parameters_list[i] = format(param, 'f') + # Ensure exactly 4 decimal places for SMALLMONEY with deterministic rounding + quantized_param = param.quantize(decimal.Decimal('0.0001'), rounding=decimal.ROUND_HALF_UP) + parameters_list[i] = f"{quantized_param:.4f}" return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, @@ -408,7 +410,9 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg False, ) if MONEY_MIN <= param <= MONEY_MAX: - parameters_list[i] = format(param, 'f') + # Ensure exactly 4 decimal places for MONEY with deterministic rounding + quantized_param = param.quantize(decimal.Decimal('0.0001'), rounding=decimal.ROUND_HALF_UP) + parameters_list[i] = f"{quantized_param:.4f}" return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index a7f2c8ae..b4182a8b 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -8022,6 +8022,84 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection): db_connection.commit() +def test_money_smallmoney_precision_handling(cursor, db_connection): + """Test MONEY/SMALLMONEY precision handling with >4 fractional digits and scientific notation""" + try: + drop_table_if_exists(cursor, "#pytest_money_precision") + cursor.execute( + """ + CREATE TABLE #pytest_money_precision ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY, + description VARCHAR(100) + ) + """ + ) + db_connection.commit() + + # Test cases for precision issues mentioned in code review + test_cases = [ + # Input with >4 fractional digits should be rounded to exactly 4 + (decimal.Decimal("1.234567"), decimal.Decimal("1.234567"), decimal.Decimal("1.2346"), "More than 4 digits - ROUND_HALF_UP"), + (decimal.Decimal("100.12345"), decimal.Decimal("100.12345"), decimal.Decimal("100.1235"), "Rounding case 1 - HALF_UP rounds up"), + (decimal.Decimal("100.12355"), decimal.Decimal("100.12355"), decimal.Decimal("100.1236"), "Rounding case 2 - HALF_UP rounds up"), + + # Scientific notation should expand and be limited to 4 decimal places + (decimal.Decimal("1E-3"), decimal.Decimal("1E-3"), decimal.Decimal("0.0010"), "Scientific notation 1E-3"), + (decimal.Decimal("1.23E-2"), decimal.Decimal("1.23E-2"), decimal.Decimal("0.0123"), "Scientific notation 1.23E-2"), + + # Boundary values with extra fractional digits + (decimal.Decimal("214748.36471234"), None, decimal.Decimal("214748.3647"), "SMALLMONEY_MAX with extra digits"), + (decimal.Decimal("922337203685477.58071234"), decimal.Decimal("214748.0000"), decimal.Decimal("922337203685477.5807"), "MONEY_MAX with extra digits"), + ] + + for i, (money_val, smallmoney_val, expected_val, description) in enumerate(test_cases): + # Skip MONEY test for SMALLMONEY boundary case and vice versa + if smallmoney_val is None: + smallmoney_val = decimal.Decimal("0.0000") + if money_val > decimal.Decimal("214748.3647"): + # For MONEY_MAX case, use a smaller SMALLMONEY value + pass + + cursor.execute( + "INSERT INTO #pytest_money_precision (m, sm, description) VALUES (?, ?, ?)", + (money_val, smallmoney_val, description) + ) + db_connection.commit() + + # Retrieve and verify the stored value has exactly 4 decimal places + cursor.execute("SELECT m, sm FROM #pytest_money_precision WHERE id = ?", (i+1,)) + result = cursor.fetchone() + + stored_money, stored_smallmoney = result + + # Verify both values have exactly 4 decimal places when converted to string + money_str = str(stored_money) + smallmoney_str = str(stored_smallmoney) + + # Both should have exactly 4 decimal places + assert '.' in money_str, f"MONEY value should have decimal point: {money_str}" + assert '.' in smallmoney_str, f"SMALLMONEY value should have decimal point: {smallmoney_str}" + + money_decimals = len(money_str.split('.')[1]) + smallmoney_decimals = len(smallmoney_str.split('.')[1]) + + assert money_decimals == 4, f"MONEY should have exactly 4 decimal places, got {money_decimals}: {money_str}" + assert smallmoney_decimals == 4, f"SMALLMONEY should have exactly 4 decimal places, got {smallmoney_decimals}: {smallmoney_str}" + + # For the expected rounding cases, verify the exact values + if expected_val: + if smallmoney_val == money_val: # Same input for both columns + assert stored_smallmoney == expected_val, f"Expected {expected_val}, got {stored_smallmoney} for {description}" + + except Exception as e: + pytest.fail(f"MONEY/SMALLMONEY precision test failed: {e}") + finally: + drop_table_if_exists(cursor, "#pytest_money_precision") + 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: From 47ea7ecf69e431772cd3db5bdbea868698a7ca52 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Tue, 4 Nov 2025 11:02:09 +0530 Subject: [PATCH 3/3] Revert "addressed review comment" This reverts commit 00b7bd03343d0d4fb5e65593c598d8595459549b. --- mssql_python/cursor.py | 8 ++--- tests/test_004_cursor.py | 78 ---------------------------------------- 2 files changed, 2 insertions(+), 84 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index db1d14bd..adfc5042 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -399,9 +399,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg # Detect MONEY / SMALLMONEY range if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX: - # Ensure exactly 4 decimal places for SMALLMONEY with deterministic rounding - quantized_param = param.quantize(decimal.Decimal('0.0001'), rounding=decimal.ROUND_HALF_UP) - parameters_list[i] = f"{quantized_param:.4f}" + parameters_list[i] = format(param, 'f') return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, @@ -410,9 +408,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg False, ) if MONEY_MIN <= param <= MONEY_MAX: - # Ensure exactly 4 decimal places for MONEY with deterministic rounding - quantized_param = param.quantize(decimal.Decimal('0.0001'), rounding=decimal.ROUND_HALF_UP) - parameters_list[i] = f"{quantized_param:.4f}" + parameters_list[i] = format(param, 'f') return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index b4182a8b..a7f2c8ae 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -8022,84 +8022,6 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection): db_connection.commit() -def test_money_smallmoney_precision_handling(cursor, db_connection): - """Test MONEY/SMALLMONEY precision handling with >4 fractional digits and scientific notation""" - try: - drop_table_if_exists(cursor, "#pytest_money_precision") - cursor.execute( - """ - CREATE TABLE #pytest_money_precision ( - id INT IDENTITY PRIMARY KEY, - m MONEY, - sm SMALLMONEY, - description VARCHAR(100) - ) - """ - ) - db_connection.commit() - - # Test cases for precision issues mentioned in code review - test_cases = [ - # Input with >4 fractional digits should be rounded to exactly 4 - (decimal.Decimal("1.234567"), decimal.Decimal("1.234567"), decimal.Decimal("1.2346"), "More than 4 digits - ROUND_HALF_UP"), - (decimal.Decimal("100.12345"), decimal.Decimal("100.12345"), decimal.Decimal("100.1235"), "Rounding case 1 - HALF_UP rounds up"), - (decimal.Decimal("100.12355"), decimal.Decimal("100.12355"), decimal.Decimal("100.1236"), "Rounding case 2 - HALF_UP rounds up"), - - # Scientific notation should expand and be limited to 4 decimal places - (decimal.Decimal("1E-3"), decimal.Decimal("1E-3"), decimal.Decimal("0.0010"), "Scientific notation 1E-3"), - (decimal.Decimal("1.23E-2"), decimal.Decimal("1.23E-2"), decimal.Decimal("0.0123"), "Scientific notation 1.23E-2"), - - # Boundary values with extra fractional digits - (decimal.Decimal("214748.36471234"), None, decimal.Decimal("214748.3647"), "SMALLMONEY_MAX with extra digits"), - (decimal.Decimal("922337203685477.58071234"), decimal.Decimal("214748.0000"), decimal.Decimal("922337203685477.5807"), "MONEY_MAX with extra digits"), - ] - - for i, (money_val, smallmoney_val, expected_val, description) in enumerate(test_cases): - # Skip MONEY test for SMALLMONEY boundary case and vice versa - if smallmoney_val is None: - smallmoney_val = decimal.Decimal("0.0000") - if money_val > decimal.Decimal("214748.3647"): - # For MONEY_MAX case, use a smaller SMALLMONEY value - pass - - cursor.execute( - "INSERT INTO #pytest_money_precision (m, sm, description) VALUES (?, ?, ?)", - (money_val, smallmoney_val, description) - ) - db_connection.commit() - - # Retrieve and verify the stored value has exactly 4 decimal places - cursor.execute("SELECT m, sm FROM #pytest_money_precision WHERE id = ?", (i+1,)) - result = cursor.fetchone() - - stored_money, stored_smallmoney = result - - # Verify both values have exactly 4 decimal places when converted to string - money_str = str(stored_money) - smallmoney_str = str(stored_smallmoney) - - # Both should have exactly 4 decimal places - assert '.' in money_str, f"MONEY value should have decimal point: {money_str}" - assert '.' in smallmoney_str, f"SMALLMONEY value should have decimal point: {smallmoney_str}" - - money_decimals = len(money_str.split('.')[1]) - smallmoney_decimals = len(smallmoney_str.split('.')[1]) - - assert money_decimals == 4, f"MONEY should have exactly 4 decimal places, got {money_decimals}: {money_str}" - assert smallmoney_decimals == 4, f"SMALLMONEY should have exactly 4 decimal places, got {smallmoney_decimals}: {smallmoney_str}" - - # For the expected rounding cases, verify the exact values - if expected_val: - if smallmoney_val == money_val: # Same input for both columns - assert stored_smallmoney == expected_val, f"Expected {expected_val}, got {stored_smallmoney} for {description}" - - except Exception as e: - pytest.fail(f"MONEY/SMALLMONEY precision test failed: {e}") - finally: - drop_table_if_exists(cursor, "#pytest_money_precision") - 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: