Skip to content

Commit 622f95b

Browse files
authored
FEAT: Add support for decimal value in scientific notation (#313)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#40022](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/40022) <!-- External contributors: GitHub Issue --> > GitHub Issue: #<ISSUE_NUMBER> ------------------------------------------------------------------- ### Summary <!-- Insert your summary of changes below. Minimum 10 characters required. --> This pull request improves the handling of decimal values, especially those in scientific notation, when converting to SQL `VARCHAR` types. The changes ensure that decimals are consistently formatted as fixed-point strings rather than potentially problematic scientific notation, preventing SQL Server conversion errors. Additionally, new tests have been added to verify correct behavior for a variety of decimal values. **Decimal formatting and conversion improvements:** * Updated `_map_sql_type` in `mssql_python/cursor.py` to use `format(param, 'f')` instead of `str(param)` for `MONEY` and `SMALLMONEY` types, ensuring decimals are always converted to fixed-point strings. [[1]](diffhunk://#diff-deceea46ae01082ce8400e14fa02f4b7585afb7b5ed9885338b66494f5f38280L402-R402) [[2]](diffhunk://#diff-deceea46ae01082ce8400e14fa02f4b7585afb7b5ed9885338b66494f5f38280L412-R411) * Modified `executemany` in `mssql_python/cursor.py` to format decimal values as fixed-point strings before insertion when mapped to `SQL_VARCHAR`. **Test suite enhancements:** * Added `test_decimal_scientific_notation_to_varchar` in `tests/test_004_cursor.py` to verify that decimals (including those with scientific notation) are correctly converted and stored as `VARCHAR` without causing conversion errors. **Test cleanup and simplification:** * Removed exception handling logic in `test_numeric_leading_zeros_precision_loss` and `test_numeric_extreme_exponents_precision_loss` that previously skipped tests on conversion errors, as these errors should no longer occur with the improved formatting. [[1]](diffhunk://#diff-82594712308ff34afa8b067af67db231e9a1372ef474da3db121e14e4d418f69L13944-L13957) [[2]](diffhunk://#diff-82594712308ff34afa8b067af67db231e9a1372ef474da3db121e14e4d418f69L14005-L14030) <!-- ### PR Title Guide > For feature requests FEAT: (short-description) > For non-feature requests like test case updates, config updates , dependency updates etc CHORE: (short-description) > For Fix requests FIX: (short-description) > For doc update requests DOC: (short-description) > For Formatting, indentation, or styling update STYLE: (short-description) > For Refactor, without any feature changes REFACTOR: (short-description) > For release related changes, without any feature changes RELEASE: #<RELEASE_VERSION> (short-description) ### Contribution Guidelines External contributors: - Create a GitHub issue first: https://github.com/microsoft/mssql-python/issues/new - Link the GitHub issue in the "GitHub Issue" section above - Follow the PR title format and provide a meaningful summary mssql-python maintainers: - Create an ADO Work Item following internal processes - Link the ADO Work Item in the "ADO Work Item" section above - Follow the PR title format and provide a meaningful summary -->
1 parent 0a5d1f2 commit 622f95b

File tree

2 files changed

+70
-40
lines changed

2 files changed

+70
-40
lines changed

mssql_python/cursor.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
395395

396396
# Detect MONEY / SMALLMONEY range
397397
if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX:
398-
# smallmoney
399-
parameters_list[i] = str(param)
398+
parameters_list[i] = format(param, 'f')
400399
return (
401400
ddbc_sql_const.SQL_VARCHAR.value,
402401
ddbc_sql_const.SQL_C_CHAR.value,
@@ -405,8 +404,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
405404
False,
406405
)
407406
if MONEY_MIN <= param <= MONEY_MAX:
408-
# money
409-
parameters_list[i] = str(param)
407+
parameters_list[i] = format(param, 'f')
410408
return (
411409
ddbc_sql_const.SQL_VARCHAR.value,
412410
ddbc_sql_const.SQL_C_CHAR.value,
@@ -1916,13 +1914,12 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
19161914
for i, val in enumerate(processed_row):
19171915
if val is None:
19181916
continue
1919-
# Convert Decimals for money/smallmoney to string
19201917
if (
19211918
isinstance(val, decimal.Decimal)
19221919
and parameters_type[i].paramSQLType
19231920
== ddbc_sql_const.SQL_VARCHAR.value
19241921
):
1925-
processed_row[i] = str(val)
1922+
processed_row[i] = format(val, 'f')
19261923
# Existing numeric conversion
19271924
elif parameters_type[i].paramSQLType in (
19281925
ddbc_sql_const.SQL_DECIMAL.value,

tests/test_004_cursor.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13477,20 +13477,6 @@ def test_numeric_leading_zeros_precision_loss(
1347713477
actual == expected
1347813478
), f"Leading zeros precision loss for {value}, expected {expected}, got {actual}"
1347913479

13480-
except Exception as e:
13481-
# Handle cases where values get converted to scientific notation and cause SQL Server conversion errors
13482-
error_msg = str(e).lower()
13483-
if (
13484-
"converting" in error_msg
13485-
and "varchar" in error_msg
13486-
and "numeric" in error_msg
13487-
):
13488-
pytest.skip(
13489-
f"Value {value} converted to scientific notation, causing expected SQL Server conversion error: {e}"
13490-
)
13491-
else:
13492-
raise # Re-raise unexpected errors
13493-
1349413480
finally:
1349513481
try:
1349613482
cursor.execute(f"DROP TABLE {table_name}")
@@ -13538,32 +13524,13 @@ def test_numeric_extreme_exponents_precision_loss(
1353813524
"1E-18"
1353913525
), f"Extreme exponent value not preserved for {description}: {value} -> {actual}"
1354013526

13541-
except Exception as e:
13542-
# Handle expected SQL Server validation errors for scientific notation values
13543-
error_msg = str(e).lower()
13544-
if "scale" in error_msg and "range" in error_msg:
13545-
# This is expected - SQL Server rejects invalid scale/precision combinations
13546-
pytest.skip(
13547-
f"Expected SQL Server scale/precision validation for {description}: {e}"
13548-
)
13549-
elif any(
13550-
keyword in error_msg
13551-
for keyword in ["converting", "overflow", "precision", "varchar", "numeric"]
13552-
):
13553-
# Other expected precision/conversion issues
13554-
pytest.skip(
13555-
f"Expected SQL Server precision limits or VARCHAR conversion for {description}: {e}"
13556-
)
13557-
else:
13558-
raise # Re-raise if it's not a precision-related error
1355913527
finally:
1356013528
try:
1356113529
cursor.execute(f"DROP TABLE {table_name}")
1356213530
db_connection.commit()
1356313531
except:
1356413532
pass # Table might not exist if creation failed
1356513533

13566-
1356713534
# ---------------------------------------------------------
1356813535
# Test 12: 38-digit precision boundary limits
1356913536
# ---------------------------------------------------------
@@ -13659,6 +13626,72 @@ def test_numeric_beyond_38_digit_precision_negative(
1365913626
), f"Expected SQL Server precision limit message for {description}, got: {error_msg}"
1366013627

1366113628

13629+
@pytest.mark.parametrize(
13630+
"values, description",
13631+
[
13632+
# Small decimal values with scientific notation
13633+
(
13634+
[
13635+
decimal.Decimal('0.70000000000696'),
13636+
decimal.Decimal('1E-7'),
13637+
decimal.Decimal('0.00001'),
13638+
decimal.Decimal('6.96E-12'),
13639+
],
13640+
"Small decimals with scientific notation"
13641+
),
13642+
# Large decimal values with scientific notation
13643+
(
13644+
[
13645+
decimal.Decimal('4E+8'),
13646+
decimal.Decimal('1.521E+15'),
13647+
decimal.Decimal('5.748E+18'),
13648+
decimal.Decimal('1E+11')
13649+
],
13650+
"Large decimals with positive exponents"
13651+
),
13652+
# Medium-sized decimals
13653+
(
13654+
[
13655+
decimal.Decimal('123.456'),
13656+
decimal.Decimal('9999.9999'),
13657+
decimal.Decimal('1000000.50')
13658+
],
13659+
"Medium-sized decimals"
13660+
),
13661+
],
13662+
)
13663+
def test_decimal_scientific_notation_to_varchar(cursor, db_connection, values, description):
13664+
"""
13665+
Test that Decimal values with scientific notation are properly converted
13666+
to VARCHAR without triggering 'varchar to numeric' conversion errors.
13667+
This verifies that the driver correctly handles Decimal to VARCHAR conversion
13668+
"""
13669+
table_name = "#pytest_decimal_varchar_conversion"
13670+
try:
13671+
cursor.execute(f"CREATE TABLE {table_name} (id INT IDENTITY(1,1), val VARCHAR(50))")
13672+
13673+
for val in values:
13674+
cursor.execute(f"INSERT INTO {table_name} (val) VALUES (?)", (val,))
13675+
db_connection.commit()
13676+
13677+
cursor.execute(f"SELECT val FROM {table_name} ORDER BY id")
13678+
rows = cursor.fetchall()
13679+
13680+
assert len(rows) == len(values), f"Expected {len(values)} rows, got {len(rows)}"
13681+
13682+
for i, (row, expected_val) in enumerate(zip(rows, values)):
13683+
stored_val = decimal.Decimal(row[0])
13684+
assert stored_val == expected_val, (
13685+
f"{description}: Row {i} mismatch - expected {expected_val}, got {stored_val}"
13686+
)
13687+
13688+
finally:
13689+
try:
13690+
cursor.execute(f"DROP TABLE {table_name}")
13691+
db_connection.commit()
13692+
except:
13693+
pass
13694+
1366213695
SMALL_XML = "<root><item>1</item></root>"
1366313696
LARGE_XML = "<root>" + "".join(f"<item>{i}</item>" for i in range(10000)) + "</root>"
1366413697
EMPTY_XML = ""
@@ -14400,4 +14433,4 @@ def test_close(db_connection):
1440014433
except Exception as e:
1440114434
pytest.fail(f"Cursor close test failed: {e}")
1440214435
finally:
14403-
cursor = db_connection.cursor()
14436+
cursor = db_connection.cursor()

0 commit comments

Comments
 (0)