Skip to content

Commit 5d50020

Browse files
authored
FEAT: Adding money and smallmoney support in executemany (#246)
### 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#34936](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/34936) [AB#34937](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/34937) <!-- 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.Decimal` values and `None` (NULL) values when using `executemany` to insert data into MONEY and SMALLMONEY columns in SQL Server. It also adds comprehensive tests to verify correct roundtrip and NULL handling for these types. Improvements to parameter processing in `executemany`: * Updated the parameter processing logic in `cursor.py` to ensure that `decimal.Decimal` values are properly converted for VARCHAR columns and that `None` values are correctly skipped during conversion. This helps prevent type errors and ensures correct database insertion for MONEY, SMALLMONEY, and related types. Expanded test coverage for MONEY and SMALLMONEY types: * Added `test_money_smallmoney_roundtrip_executemany` to verify that inserting and retrieving MONEY and SMALLMONEY values using `executemany` with `decimal.Decimal` works as expected, including checks for value equality and type. * Added `test_money_smallmoney_executemany_null_handling` to ensure that inserting NULLs into MONEY and SMALLMONEY columns via `executemany` behaves correctly and returns the expected results. <!-- ### 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 fba0e63 commit 5d50020

File tree

2 files changed

+145
-36
lines changed

2 files changed

+145
-36
lines changed

mssql_python/cursor.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,14 +1712,23 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None:
17121712
for row in seq_of_parameters:
17131713
processed_row = list(row)
17141714
for i, val in enumerate(processed_row):
1715-
if (parameters_type[i].paramSQLType in
1715+
if val is None:
1716+
continue
1717+
# Convert Decimals for money/smallmoney to string
1718+
if isinstance(val, decimal.Decimal) and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value:
1719+
processed_row[i] = str(val)
1720+
# Existing numeric conversion
1721+
elif (parameters_type[i].paramSQLType in
17161722
(ddbc_sql_const.SQL_DECIMAL.value, ddbc_sql_const.SQL_NUMERIC.value) and
1717-
not isinstance(val, decimal.Decimal) and val is not None):
1723+
not isinstance(val, decimal.Decimal)):
17181724
try:
17191725
processed_row[i] = decimal.Decimal(str(val))
1720-
except:
1721-
pass # Keep original value if conversion fails
1726+
except Exception as e:
1727+
raise ValueError(
1728+
f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}"
1729+
)
17221730
processed_parameters.append(processed_row)
1731+
17231732

17241733
# Now transpose the processed parameters
17251734
columnwise_params, row_count = self._transpose_rowwise_to_columnwise(processed_parameters)

tests/test_004_cursor.py

Lines changed: 132 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6729,9 +6729,9 @@ def test_nvarcharmax_large(cursor, db_connection):
67296729
def test_money_smallmoney_insert_fetch(cursor, db_connection):
67306730
"""Test inserting and retrieving valid MONEY and SMALLMONEY values including boundaries and typical data"""
67316731
try:
6732-
drop_table_if_exists(cursor, "dbo.money_test")
6732+
drop_table_if_exists(cursor, "#pytest_money_test")
67336733
cursor.execute("""
6734-
CREATE TABLE dbo.money_test (
6734+
CREATE TABLE #pytest_money_test (
67356735
id INT IDENTITY PRIMARY KEY,
67366736
m MONEY,
67376737
sm SMALLMONEY,
@@ -6742,27 +6742,27 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection):
67426742
db_connection.commit()
67436743

67446744
# Max values
6745-
cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
6745+
cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
67466746
(decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"),
67476747
decimal.Decimal("9999999999999.9999"), decimal.Decimal("1234.5678")))
67486748

67496749
# Min values
6750-
cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
6750+
cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
67516751
(decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"),
67526752
decimal.Decimal("-9999999999999.9999"), decimal.Decimal("-1234.5678")))
67536753

67546754
# Typical values
6755-
cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
6755+
cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
67566756
(decimal.Decimal("1234567.8901"), decimal.Decimal("12345.6789"),
67576757
decimal.Decimal("42.4242"), decimal.Decimal("3.1415")))
67586758

67596759
# NULL values
6760-
cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
6760+
cursor.execute("INSERT INTO #pytest_money_test (m, sm, d, n) VALUES (?, ?, ?, ?)",
67616761
(None, None, None, None))
67626762

67636763
db_connection.commit()
67646764

6765-
cursor.execute("SELECT m, sm, d, n FROM dbo.money_test ORDER BY id")
6765+
cursor.execute("SELECT m, sm, d, n FROM #pytest_money_test ORDER BY id")
67666766
results = cursor.fetchall()
67676767
assert len(results) == 4, f"Expected 4 rows, got {len(results)}"
67686768

@@ -6787,16 +6787,15 @@ def test_money_smallmoney_insert_fetch(cursor, db_connection):
67876787
except Exception as e:
67886788
pytest.fail(f"MONEY and SMALLMONEY insert/fetch test failed: {e}")
67896789
finally:
6790-
drop_table_if_exists(cursor, "dbo.money_test")
6790+
drop_table_if_exists(cursor, "#pytest_money_test")
67916791
db_connection.commit()
67926792

67936793

67946794
def test_money_smallmoney_null_handling(cursor, db_connection):
67956795
"""Test that NULL values for MONEY and SMALLMONEY are stored and retrieved correctly"""
67966796
try:
6797-
drop_table_if_exists(cursor, "dbo.money_test")
67986797
cursor.execute("""
6799-
CREATE TABLE dbo.money_test (
6798+
CREATE TABLE #pytest_money_test (
68006799
id INT IDENTITY PRIMARY KEY,
68016800
m MONEY,
68026801
sm SMALLMONEY
@@ -6805,19 +6804,19 @@ def test_money_smallmoney_null_handling(cursor, db_connection):
68056804
db_connection.commit()
68066805

68076806
# Row with both NULLs
6808-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", (None, None))
6807+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", (None, None))
68096808

68106809
# Row with m filled, sm NULL
6811-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)",
6810+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)",
68126811
(decimal.Decimal("123.4500"), None))
68136812

68146813
# Row with m NULL, sm filled
6815-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)",
6814+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)",
68166815
(None, decimal.Decimal("67.8900")))
68176816

68186817
db_connection.commit()
68196818

6820-
cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id")
6819+
cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id")
68216820
results = cursor.fetchall()
68226821
assert len(results) == 3, f"Expected 3 rows, got {len(results)}"
68236822

@@ -6838,16 +6837,15 @@ def test_money_smallmoney_null_handling(cursor, db_connection):
68386837
except Exception as e:
68396838
pytest.fail(f"MONEY and SMALLMONEY NULL handling test failed: {e}")
68406839
finally:
6841-
drop_table_if_exists(cursor, "dbo.money_test")
6840+
drop_table_if_exists(cursor, "#pytest_money_test")
68426841
db_connection.commit()
68436842

68446843

68456844
def test_money_smallmoney_roundtrip(cursor, db_connection):
68466845
"""Test inserting and retrieving MONEY and SMALLMONEY using decimal.Decimal roundtrip"""
68476846
try:
6848-
drop_table_if_exists(cursor, "dbo.money_test")
68496847
cursor.execute("""
6850-
CREATE TABLE dbo.money_test (
6848+
CREATE TABLE #pytest_money_test (
68516849
id INT IDENTITY PRIMARY KEY,
68526850
m MONEY,
68536851
sm SMALLMONEY
@@ -6856,10 +6854,10 @@ def test_money_smallmoney_roundtrip(cursor, db_connection):
68566854
db_connection.commit()
68576855

68586856
values = (decimal.Decimal("12345.6789"), decimal.Decimal("987.6543"))
6859-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", values)
6857+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", values)
68606858
db_connection.commit()
68616859

6862-
cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC")
6860+
cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id DESC")
68636861
row = cursor.fetchone()
68646862
for i, (val, exp_val) in enumerate(zip(row, values), 1):
68656863
assert val == exp_val, f"col{i} roundtrip mismatch, got {val}, expected {exp_val}"
@@ -6868,16 +6866,16 @@ def test_money_smallmoney_roundtrip(cursor, db_connection):
68686866
except Exception as e:
68696867
pytest.fail(f"MONEY and SMALLMONEY roundtrip test failed: {e}")
68706868
finally:
6871-
drop_table_if_exists(cursor, "dbo.money_test")
6869+
drop_table_if_exists(cursor, "#pytest_money_test")
68726870
db_connection.commit()
68736871

68746872

68756873
def test_money_smallmoney_boundaries(cursor, db_connection):
68766874
"""Test boundary values for MONEY and SMALLMONEY types are handled correctly"""
68776875
try:
6878-
drop_table_if_exists(cursor, "dbo.money_test")
6876+
drop_table_if_exists(cursor, "#pytest_money_test")
68796877
cursor.execute("""
6880-
CREATE TABLE dbo.money_test (
6878+
CREATE TABLE #pytest_money_test (
68816879
id INT IDENTITY PRIMARY KEY,
68826880
m MONEY,
68836881
sm SMALLMONEY
@@ -6886,16 +6884,16 @@ def test_money_smallmoney_boundaries(cursor, db_connection):
68866884
db_connection.commit()
68876885

68886886
# Insert max boundary
6889-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)",
6887+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)",
68906888
(decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647")))
68916889

68926890
# Insert min boundary
6893-
cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)",
6891+
cursor.execute("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)",
68946892
(decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648")))
68956893

68966894
db_connection.commit()
68976895

6898-
cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC")
6896+
cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id DESC")
68996897
results = cursor.fetchall()
69006898
expected = [
69016899
(decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648")),
@@ -6909,16 +6907,15 @@ def test_money_smallmoney_boundaries(cursor, db_connection):
69096907
except Exception as e:
69106908
pytest.fail(f"MONEY and SMALLMONEY boundary values test failed: {e}")
69116909
finally:
6912-
drop_table_if_exists(cursor, "dbo.money_test")
6910+
drop_table_if_exists(cursor, "#pytest_money_test")
69136911
db_connection.commit()
69146912

69156913

69166914
def test_money_smallmoney_invalid_values(cursor, db_connection):
69176915
"""Test that invalid or out-of-range MONEY and SMALLMONEY values raise errors"""
69186916
try:
6919-
drop_table_if_exists(cursor, "dbo.money_test")
69206917
cursor.execute("""
6921-
CREATE TABLE dbo.money_test (
6918+
CREATE TABLE #pytest_money_test (
69226919
id INT IDENTITY PRIMARY KEY,
69236920
m MONEY,
69246921
sm SMALLMONEY
@@ -6928,20 +6925,123 @@ def test_money_smallmoney_invalid_values(cursor, db_connection):
69286925

69296926
# Out of range MONEY
69306927
with pytest.raises(Exception):
6931-
cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", (decimal.Decimal("922337203685477.5808"),))
6928+
cursor.execute("INSERT INTO #pytest_money_test (m) VALUES (?)", (decimal.Decimal("922337203685477.5808"),))
69326929

69336930
# Out of range SMALLMONEY
69346931
with pytest.raises(Exception):
6935-
cursor.execute("INSERT INTO dbo.money_test (sm) VALUES (?)", (decimal.Decimal("214748.3648"),))
6932+
cursor.execute("INSERT INTO #pytest_money_test (sm) VALUES (?)", (decimal.Decimal("214748.3648"),))
69366933

69376934
# Invalid string
69386935
with pytest.raises(Exception):
6939-
cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", ("invalid_string",))
6936+
cursor.execute("INSERT INTO #pytest_money_test (m) VALUES (?)", ("invalid_string",))
69406937

69416938
except Exception as e:
69426939
pytest.fail(f"MONEY and SMALLMONEY invalid values test failed: {e}")
69436940
finally:
6944-
drop_table_if_exists(cursor, "dbo.money_test")
6941+
drop_table_if_exists(cursor, "#pytest_money_test")
6942+
db_connection.commit()
6943+
6944+
def test_money_smallmoney_roundtrip_executemany(cursor, db_connection):
6945+
"""Test inserting and retrieving MONEY and SMALLMONEY using executemany with decimal.Decimal"""
6946+
try:
6947+
cursor.execute("""
6948+
CREATE TABLE #pytest_money_test (
6949+
id INT IDENTITY PRIMARY KEY,
6950+
m MONEY,
6951+
sm SMALLMONEY
6952+
)
6953+
""")
6954+
db_connection.commit()
6955+
6956+
test_data = [
6957+
(decimal.Decimal("12345.6789"), decimal.Decimal("987.6543")),
6958+
(decimal.Decimal("0.0001"), decimal.Decimal("0.01")),
6959+
(None, decimal.Decimal("42.42")),
6960+
(decimal.Decimal("-1000.99"), None),
6961+
]
6962+
6963+
# Insert using executemany directly with Decimals
6964+
cursor.executemany(
6965+
"INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)",
6966+
test_data
6967+
)
6968+
db_connection.commit()
6969+
6970+
cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id")
6971+
results = cursor.fetchall()
6972+
assert len(results) == len(test_data)
6973+
6974+
for i, (row, expected) in enumerate(zip(results, test_data), 1):
6975+
for j, (val, exp_val) in enumerate(zip(row, expected), 1):
6976+
if exp_val is None:
6977+
assert val is None
6978+
else:
6979+
assert val == exp_val
6980+
assert isinstance(val, decimal.Decimal)
6981+
6982+
finally:
6983+
drop_table_if_exists(cursor, "#pytest_money_test")
6984+
db_connection.commit()
6985+
6986+
6987+
def test_money_smallmoney_executemany_null_handling(cursor, db_connection):
6988+
"""Test inserting NULLs into MONEY and SMALLMONEY using executemany"""
6989+
try:
6990+
cursor.execute("""
6991+
CREATE TABLE #pytest_money_test (
6992+
id INT IDENTITY PRIMARY KEY,
6993+
m MONEY,
6994+
sm SMALLMONEY
6995+
)
6996+
""")
6997+
db_connection.commit()
6998+
6999+
rows = [
7000+
(None, None),
7001+
(decimal.Decimal("123.4500"), None),
7002+
(None, decimal.Decimal("67.8900")),
7003+
]
7004+
cursor.executemany("INSERT INTO #pytest_money_test (m, sm) VALUES (?, ?)", rows)
7005+
db_connection.commit()
7006+
7007+
cursor.execute("SELECT m, sm FROM #pytest_money_test ORDER BY id ASC")
7008+
results = cursor.fetchall()
7009+
assert len(results) == len(rows)
7010+
7011+
for row, expected in zip(results, rows):
7012+
for val, exp_val in zip(row, expected):
7013+
if exp_val is None:
7014+
assert val is None
7015+
else:
7016+
assert val == exp_val
7017+
assert isinstance(val, decimal.Decimal)
7018+
7019+
finally:
7020+
drop_table_if_exists(cursor, "#pytest_money_test")
7021+
db_connection.commit()
7022+
7023+
def test_money_smallmoney_out_of_range_low(cursor, db_connection):
7024+
"""Test inserting values just below the minimum MONEY/SMALLMONEY range raises error"""
7025+
try:
7026+
drop_table_if_exists(cursor, "#pytest_money_test")
7027+
cursor.execute("CREATE TABLE #pytest_money_test (m MONEY, sm SMALLMONEY)")
7028+
db_connection.commit()
7029+
7030+
# Just below minimum MONEY
7031+
with pytest.raises(Exception):
7032+
cursor.execute(
7033+
"INSERT INTO #pytest_money_test (m) VALUES (?)",
7034+
(decimal.Decimal("-922337203685477.5809"),)
7035+
)
7036+
7037+
# Just below minimum SMALLMONEY
7038+
with pytest.raises(Exception):
7039+
cursor.execute(
7040+
"INSERT INTO #pytest_money_test (sm) VALUES (?)",
7041+
(decimal.Decimal("-214748.3649"),)
7042+
)
7043+
finally:
7044+
drop_table_if_exists(cursor, "#pytest_money_test")
69457045
db_connection.commit()
69467046

69477047
def test_uuid_insert_and_select_none(cursor, db_connection):

0 commit comments

Comments
 (0)