@@ -11562,6 +11562,176 @@ 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+ # 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+ # Test 11: Extreme exponents precision loss
11612+ # ---------------------------------------------------------
11613+ @pytest .mark .parametrize ("value, description" , [
11614+ (decimal .Decimal ('1E-20' ), "1E-20 exponent" ),
11615+ (decimal .Decimal ('1E-38' ), "1E-38 exponent" ),
11616+ (decimal .Decimal ('5E-35' ), "5E-35 exponent" ),
11617+ (decimal .Decimal ('9E-30' ), "9E-30 exponent" ),
11618+ (decimal .Decimal ('2.5E-25' ), "2.5E-25 exponent" )
11619+ ])
11620+ def test_numeric_extreme_exponents_precision_loss (cursor , db_connection , value , description ):
11621+ """Test precision loss with values having extreme small magnitudes"""
11622+ # Scientific notation values like 1E-20 create scale > precision situations
11623+ # that violate SQL Server's NUMERIC(P,S) rules - this is expected behavior
11624+
11625+ table_name = "#pytest_numeric_extreme_exp"
11626+ try :
11627+ # Try with a reasonable precision/scale that should handle most cases
11628+ cursor .execute (f"CREATE TABLE { table_name } (val NUMERIC(38, 20))" )
11629+ cursor .execute (f"INSERT INTO { table_name } (val) VALUES (?)" , (value ,))
11630+ db_connection .commit ()
11631+
11632+ cursor .execute (f"SELECT val FROM { table_name } " )
11633+ row = cursor .fetchone ()
11634+ assert row is not None , "Expected one row to be returned"
11635+
11636+ # Verify the value was stored and retrieved
11637+ actual = row [0 ]
11638+ print (f"✅ { description } : { value } -> { actual } " )
11639+
11640+ # For extreme small values, check they're mathematically equivalent
11641+ assert abs (actual - value ) < decimal .Decimal ('1E-18' ), \
11642+ f"Extreme exponent value not preserved for { description } : { value } -> { actual } "
11643+
11644+ except Exception as e :
11645+ # Handle expected SQL Server validation errors for scientific notation values
11646+ error_msg = str (e ).lower ()
11647+ if "scale" in error_msg and "range" in error_msg :
11648+ # This is expected - SQL Server rejects invalid scale/precision combinations
11649+ pytest .skip (f"Expected SQL Server scale/precision validation for { description } : { e } " )
11650+ elif any (keyword in error_msg for keyword in ["converting" , "overflow" , "precision" , "varchar" , "numeric" ]):
11651+ # Other expected precision/conversion issues
11652+ pytest .skip (f"Expected SQL Server precision limits or VARCHAR conversion for { description } : { e } " )
11653+ else :
11654+ raise # Re-raise if it's not a precision-related error
11655+ finally :
11656+ try :
11657+ cursor .execute (f"DROP TABLE { table_name } " )
11658+ db_connection .commit ()
11659+ except :
11660+ pass # Table might not exist if creation failed
11661+
11662+ # ---------------------------------------------------------
11663+ # Test 12: 38-digit precision boundary limits
11664+ # ---------------------------------------------------------
11665+ @pytest .mark .parametrize ("value" , [
11666+ # 38 digits with negative exponent
11667+ decimal .Decimal ('0.' + '0' * 36 + '1' ), # 38 digits total (1 + 37 decimal places)
11668+ # very large numbers at 38-digit limit
11669+ decimal .Decimal ('9' * 38 ), # Maximum 38-digit integer
11670+ decimal .Decimal ('1' + '0' * 37 ), # Large 38-digit number
11671+ # Additional boundary cases
11672+ decimal .Decimal ('0.' + '0' * 35 + '12' ), # 37 total digits
11673+ decimal .Decimal ('0.' + '0' * 34 + '123' ), # 36 total digits
11674+ decimal .Decimal ('0.' + '1' * 37 ), # All 1's in decimal part
11675+ decimal .Decimal ('1.' + '9' * 36 ), # Close to maximum with integer part
11676+ ])
11677+ def test_numeric_precision_boundary_limits (cursor , db_connection , value ):
11678+ """Test precision loss with values close to the 38-digit precision limit"""
11679+ precision , scale = 38 , 37 # Maximum precision with high scale
11680+ table_name = "#pytest_numeric_boundary_limits"
11681+ try :
11682+ cursor .execute (f"CREATE TABLE { table_name } (val NUMERIC({ precision } , { scale } ))" )
11683+ cursor .execute (f"INSERT INTO { table_name } (val) VALUES (?)" , (value ,))
11684+ db_connection .commit ()
11685+
11686+ cursor .execute (f"SELECT val FROM { table_name } " )
11687+ row = cursor .fetchone ()
11688+ assert row is not None , "Expected one row to be returned"
11689+
11690+ # Ensure implementation behaves correctly even at the boundaries of SQL Server's maximum precision
11691+ assert row [0 ] == value , f"Boundary precision loss for { value } , got { row [0 ]} "
11692+
11693+ except Exception as e :
11694+ # Some boundary values might exceed SQL Server limits
11695+ pytest .skip (f"Value { value } may exceed SQL Server precision limits: { e } " )
11696+ finally :
11697+ try :
11698+ cursor .execute (f"DROP TABLE { table_name } " )
11699+ db_connection .commit ()
11700+ except :
11701+ pass # Table might not exist if creation failed
11702+
11703+ # ---------------------------------------------------------
11704+ # Test 13: Negative test - Values exceeding 38-digit precision limit
11705+ # ---------------------------------------------------------
11706+ @pytest .mark .parametrize ("value, description" , [
11707+ (decimal .Decimal ('1' + '0' * 38 ), "39 digits integer" ), # 39 digits
11708+ (decimal .Decimal ('9' * 39 ), "39 nines" ), # 39 digits of 9s
11709+ (decimal .Decimal ('12345678901234567890123456789012345678901234567890' ), "50 digits" ), # 50 digits
11710+ (decimal .Decimal ('0.111111111111111111111111111111111111111' ), "39 decimal places" ), # 39 decimal digits
11711+ (decimal .Decimal ('1' * 20 + '.' + '9' * 20 ), "40 total digits" ), # 40 total digits (20+20)
11712+ (decimal .Decimal ('123456789012345678901234567890.12345678901234567' ), "47 total digits" ), # 47 total digits
11713+ ])
11714+ def test_numeric_beyond_38_digit_precision_negative (cursor , db_connection , value , description ):
11715+ """
11716+ Negative test: Ensure proper error handling for values exceeding SQL Server's 38-digit precision limit.
11717+
11718+ After our precision validation fix, mssql-python should now gracefully reject values with precision > 38
11719+ by raising a ValueError with a clear message, matching pyodbc behavior.
11720+ """
11721+ # These values should be rejected by our precision validation
11722+ with pytest .raises (ValueError ) as exc_info :
11723+ cursor .execute ("SELECT ?" , (value ,))
11724+
11725+ error_msg = str (exc_info .value )
11726+ assert "Precision of the numeric value is too high" in error_msg , \
11727+ f"Expected precision error message for { description } , got: { error_msg } "
11728+ assert "maximum precision supported by SQL Server is 38" in error_msg , \
11729+ f"Expected SQL Server precision limit message for { description } , got: { error_msg } "
11730+
11731+ print (f"✅ Correctly rejected { description } : { value } " )
11732+
11733+
11734+
1156511735def test_close (db_connection ):
1156611736 """Test closing the cursor"""
1156711737 try :
0 commit comments