@@ -11562,6 +11562,178 @@ def test_numeric_executemany(cursor, db_connection, values):
1156211562 cursor .execute (f"DROP TABLE { table_name } " )
1156311563 db_connection .commit ()
1156411564
11565+ # ---------------------------------------------------------
11566+ # Test 10: Leading zeros precision loss
11567+ # ---------------------------------------------------------
11568+ @pytest .mark .parametrize ("value, expected_precision, expected_scale" , [
11569+ # Review 1 cases: Leading zeros (using values that won't become scientific notation)
11570+ (decimal .Decimal ('000000123.45' ), 38 , 2 ), # Leading zeros in integer part
11571+ (decimal .Decimal ('000.0001234' ), 38 , 7 ), # Leading zeros in decimal part
11572+ (decimal .Decimal ('0000000000000.123456789' ), 38 , 9 ), # Many leading zeros + decimal
11573+ (decimal .Decimal ('000000.000000123456' ), 38 , 12 ) # Lots of leading zeros (avoiding E notation)
11574+ ])
11575+ def test_numeric_leading_zeros_precision_loss (cursor , db_connection , value , expected_precision , expected_scale ):
11576+ """Test precision loss with values containing lots of leading zeros"""
11577+ table_name = "#pytest_numeric_leading_zeros"
11578+ try :
11579+ # Use explicit precision and scale to avoid scientific notation issues
11580+ cursor .execute (f"CREATE TABLE { table_name } (val NUMERIC({ expected_precision } , { expected_scale } ))" )
11581+ cursor .execute (f"INSERT INTO { table_name } (val) VALUES (?)" , (value ,))
11582+ db_connection .commit ()
11583+
11584+ cursor .execute (f"SELECT val FROM { table_name } " )
11585+ row = cursor .fetchone ()
11586+ assert row is not None , "Expected one row to be returned"
11587+
11588+ # Normalize both values to the same scale for comparison
11589+ expected = value .quantize (decimal .Decimal (f"1e-{ expected_scale } " ))
11590+ actual = row [0 ]
11591+
11592+ # Verify that leading zeros are handled correctly during conversion and roundtrip
11593+ assert actual == expected , f"Leading zeros precision loss for { value } , expected { expected } , got { actual } "
11594+
11595+ except Exception as e :
11596+ # Handle cases where values get converted to scientific notation and cause SQL Server conversion errors
11597+ error_msg = str (e ).lower ()
11598+ if "converting" in error_msg and "varchar" in error_msg and "numeric" in error_msg :
11599+ pytest .skip (f"Value { value } converted to scientific notation, causing expected SQL Server conversion error: { e } " )
11600+ else :
11601+ raise # Re-raise unexpected errors
11602+
11603+ finally :
11604+ try :
11605+ cursor .execute (f"DROP TABLE { table_name } " )
11606+ db_connection .commit ()
11607+ except :
11608+ pass
11609+
11610+
11611+
11612+ # ---------------------------------------------------------
11613+ # Test 11: Extreme exponents precision loss
11614+ # ---------------------------------------------------------
11615+ @pytest .mark .parametrize ("value, description" , [
11616+ (decimal .Decimal ('1E-20' ), "1E-20 exponent" ),
11617+ (decimal .Decimal ('1E-38' ), "1E-38 exponent" ),
11618+ (decimal .Decimal ('5E-35' ), "5E-35 exponent" ),
11619+ (decimal .Decimal ('9E-30' ), "9E-30 exponent" ),
11620+ (decimal .Decimal ('2.5E-25' ), "2.5E-25 exponent" )
11621+ ])
11622+ def test_numeric_extreme_exponents_precision_loss (cursor , db_connection , value , description ):
11623+ """Test precision loss with values having extreme small magnitudes"""
11624+ # Scientific notation values like 1E-20 create scale > precision situations
11625+ # that violate SQL Server's NUMERIC(P,S) rules - this is expected behavior
11626+
11627+ table_name = "#pytest_numeric_extreme_exp"
11628+ try :
11629+ # Try with a reasonable precision/scale that should handle most cases
11630+ cursor .execute (f"CREATE TABLE { table_name } (val NUMERIC(38, 20))" )
11631+ cursor .execute (f"INSERT INTO { table_name } (val) VALUES (?)" , (value ,))
11632+ db_connection .commit ()
11633+
11634+ cursor .execute (f"SELECT val FROM { table_name } " )
11635+ row = cursor .fetchone ()
11636+ assert row is not None , "Expected one row to be returned"
11637+
11638+ # Verify the value was stored and retrieved
11639+ actual = row [0 ]
11640+ print (f"✅ { description } : { value } -> { actual } " )
11641+
11642+ # For extreme small values, check they're mathematically equivalent
11643+ assert abs (actual - value ) < decimal .Decimal ('1E-18' ), \
11644+ f"Extreme exponent value not preserved for { description } : { value } -> { actual } "
11645+
11646+ except Exception as e :
11647+ # Handle expected SQL Server validation errors for scientific notation values
11648+ error_msg = str (e ).lower ()
11649+ if "scale" in error_msg and "range" in error_msg :
11650+ # This is expected - SQL Server rejects invalid scale/precision combinations
11651+ pytest .skip (f"Expected SQL Server scale/precision validation for { description } : { e } " )
11652+ elif any (keyword in error_msg for keyword in ["converting" , "overflow" , "precision" , "varchar" , "numeric" ]):
11653+ # Other expected precision/conversion issues
11654+ pytest .skip (f"Expected SQL Server precision limits or VARCHAR conversion for { description } : { e } " )
11655+ else :
11656+ raise # Re-raise if it's not a precision-related error
11657+ finally :
11658+ try :
11659+ cursor .execute (f"DROP TABLE { table_name } " )
11660+ db_connection .commit ()
11661+ except :
11662+ pass # Table might not exist if creation failed
11663+
11664+ # ---------------------------------------------------------
11665+ # Test 12: 38-digit precision boundary limits
11666+ # ---------------------------------------------------------
11667+ @pytest .mark .parametrize ("value" , [
11668+ # Review case: 38 digits with negative exponent
11669+ decimal .Decimal ('0.' + '0' * 36 + '1' ), # 38 digits total (1 + 37 decimal places)
11670+ # Review case: very large numbers at 38-digit limit
11671+ decimal .Decimal ('9' * 38 ), # Maximum 38-digit integer
11672+ decimal .Decimal ('1' + '0' * 37 ), # Large 38-digit number
11673+ # Additional boundary cases
11674+ decimal .Decimal ('0.' + '0' * 35 + '12' ), # 37 total digits
11675+ decimal .Decimal ('0.' + '0' * 34 + '123' ), # 36 total digits
11676+ decimal .Decimal ('0.' + '1' * 37 ), # All 1's in decimal part
11677+ decimal .Decimal ('1.' + '9' * 36 ), # Close to maximum with integer part
11678+ ])
11679+ def test_numeric_precision_boundary_limits (cursor , db_connection , value ):
11680+ """Test precision loss with values close to the 38-digit precision limit"""
11681+ precision , scale = 38 , 37 # Maximum precision with high scale
11682+ table_name = "#pytest_numeric_boundary_limits"
11683+ try :
11684+ cursor .execute (f"CREATE TABLE { table_name } (val NUMERIC({ precision } , { scale } ))" )
11685+ cursor .execute (f"INSERT INTO { table_name } (val) VALUES (?)" , (value ,))
11686+ db_connection .commit ()
11687+
11688+ cursor .execute (f"SELECT val FROM { table_name } " )
11689+ row = cursor .fetchone ()
11690+ assert row is not None , "Expected one row to be returned"
11691+
11692+ # Ensure implementation behaves correctly even at the boundaries of SQL Server's maximum precision
11693+ assert row [0 ] == value , f"Boundary precision loss for { value } , got { row [0 ]} "
11694+
11695+ except Exception as e :
11696+ # Some boundary values might exceed SQL Server limits
11697+ pytest .skip (f"Value { value } may exceed SQL Server precision limits: { e } " )
11698+ finally :
11699+ try :
11700+ cursor .execute (f"DROP TABLE { table_name } " )
11701+ db_connection .commit ()
11702+ except :
11703+ pass # Table might not exist if creation failed
11704+
11705+ # ---------------------------------------------------------
11706+ # Test 13: Negative test - Values exceeding 38-digit precision limit
11707+ # ---------------------------------------------------------
11708+ @pytest .mark .parametrize ("value, description" , [
11709+ (decimal .Decimal ('1' + '0' * 38 ), "39 digits integer" ), # 39 digits
11710+ (decimal .Decimal ('9' * 39 ), "39 nines" ), # 39 digits of 9s
11711+ (decimal .Decimal ('12345678901234567890123456789012345678901234567890' ), "50 digits" ), # 50 digits
11712+ (decimal .Decimal ('0.111111111111111111111111111111111111111' ), "39 decimal places" ), # 39 decimal digits
11713+ (decimal .Decimal ('1' * 20 + '.' + '9' * 20 ), "40 total digits" ), # 40 total digits (20+20)
11714+ (decimal .Decimal ('123456789012345678901234567890.12345678901234567' ), "47 total digits" ), # 47 total digits
11715+ ])
11716+ def test_numeric_beyond_38_digit_precision_negative (cursor , db_connection , value , description ):
11717+ """
11718+ Negative test: Ensure proper error handling for values exceeding SQL Server's 38-digit precision limit.
11719+
11720+ After our precision validation fix, mssql-python should now gracefully reject values with precision > 38
11721+ by raising a ValueError with a clear message, matching pyodbc behavior.
11722+ """
11723+ # These values should be rejected by our precision validation
11724+ with pytest .raises (ValueError ) as exc_info :
11725+ cursor .execute ("SELECT ?" , (value ,))
11726+
11727+ error_msg = str (exc_info .value )
11728+ assert "Precision of the numeric value is too high" in error_msg , \
11729+ f"Expected precision error message for { description } , got: { error_msg } "
11730+ assert "maximum precision supported by SQL Server is 38" in error_msg , \
11731+ f"Expected SQL Server precision limit message for { description } , got: { error_msg } "
11732+
11733+ print (f"✅ Correctly rejected { description } : { value } " )
11734+
11735+
11736+
1156511737def test_close (db_connection ):
1156611738 """Test closing the cursor"""
1156711739 try :
0 commit comments