Skip to content

FIX: bind executemany money-range Decimals as SQL_NUMERIC - #752

Open
VyrnSynx (vyrnsynx) wants to merge 3 commits into
microsoft:mainfrom
vyrnsynx:fix-745-executemany-decimal-sql-numeric
Open

FIX: bind executemany money-range Decimals as SQL_NUMERIC#752
VyrnSynx (vyrnsynx) wants to merge 3 commits into
microsoft:mainfrom
vyrnsynx:fix-745-executemany-decimal-sql-numeric

Conversation

@vyrnsynx

Copy link
Copy Markdown

Work Item / Issue Reference

GitHub Issue: #745


Summary

executemany auto-detect still used the MONEY/SMALLMONEY-range VARCHAR shortcut after #742 fixed execute(). A money-range Decimal compared against a smaller numeric column could still overflow on that path.

This change binds auto-detected Decimal columns as SQL_NUMERIC with a batch-wide precision/scale (values still go through the existing SQL_C_CHAR string conversion). The setinputsizes DECIMAL/NUMERIC string path (GH-503) and mixed-sign VARCHAR sizing (GH-557) are left alone.

Fixes #745

executemany auto-detect skipped the money-range VARCHAR shortcut by
deriving a batch-wide NUMERIC precision/scale, matching execute() so
comparisons against smaller numeric columns no longer overflow.

setinputsizes DECIMAL/NUMERIC string binding is unchanged.

Fixes microsoft#745
Copilot AI lite review requested due to automatic review settings September 4, 2026 04:43
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new batch precision/scale path needs stricter validation and the current executemany numeric override still conflates numeric precision with string buffer sizing in a way that can produce invalid SQL Server precision (>38) for some Decimal shapes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes the remaining executemany() auto-detection path where MONEY/SMALLMONEY-range Decimal values could still be bound as SQL_VARCHAR, causing SQL Server to overflow when comparing against smaller numeric/decimal columns. The change aligns executemany() with the already-fixed execute() behavior by binding auto-detected Decimal columns as SQL_NUMERIC using a batch-wide precision/scale, while still sending values as SQL_C_CHAR strings.

Changes:

  • Update executemany() auto-detect to bind Decimal columns as SQL_NUMERIC (skipping the MONEY-range VARCHAR shortcut) and compute a batch-wide (precision, scale).
  • Add unit/integration tests covering the GH-745 overflow regression and mixed-sign batch behavior.
  • Document the behavior change in the changelog and update explanatory test/module docs.
File summaries
File Description
mssql_python/cursor.py Adds batch-wide Decimal precision/scale derivation and applies it to executemany() auto-detect numeric binding.
tests/test_020_money_smallmoney.py Adds DB integration coverage for GH-745 (no overflow on executemany comparisons; mixed-sign batch still works).
tests/test_004_cursor.py Adds unit tests for batch precision/scale derivation and verifies executemany binds money-range Decimals as SQL_NUMERIC.
CHANGELOG.md Records GH-745 fix and clarifies setinputsizes behavior remains unchanged.
Review details

Suppressed comments (1)

mssql_python/cursor.py:2729

  • executemany’s SQL_NUMERIC/SQL_DECIMAL override reuses ParamInfo.columnSize both as the numeric precision (passed as cbColDef to SQLBindParameter) and as the max SQL_C_CHAR buffer length. Setting columnSize to max_decimal_len (which includes sign and decimal point) can push the declared numeric precision above SQL Server’s max 38 (e.g., scale-38 values format to 40 chars), causing bind failures unrelated to the actual numeric precision/scale. Consider decoupling numeric precision from string buffer sizing (e.g., compute buffer sizes from actual encoded string lengths in BindParameterArray when SQLType is NUMERIC/DECIMAL but keep cbColDef=precision<=38).
                    # Ensure columnSize also accommodates the longest string form
                    # (mixed-sign batches, GH-557).
                    if max_decimal_len > paraminfo.columnSize:
                        paraminfo.columnSize = max_decimal_len
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mssql_python/cursor.py
Comment on lines +2402 to +2417
max_scale = 0
max_int_digits = 0
found = False
for value in column:
if not isinstance(value, decimal.Decimal):
continue
try:
precision, scale = self._decimal_sql_precision_scale(value)
except ValueError:
continue
found = True
max_scale = max(max_scale, scale)
max_int_digits = max(max_int_digits, precision - scale)
if not found:
return 0, 0
return max(max_int_digits + max_scale, 1), max_scale

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the change that routes executemany auto-detected Decimal columns to SQL_NUMERIC (GH-745). The core approach is sound and the new _decimal_sql_precision_scale / _batch_decimal_precision_scale helpers correctly derive a batch-wide precision/scale (and fix a latent sample-only sizing gap). Verified the precision/scale math matches _get_numeric_data, and black --check passes.

One blocking issue: the retained max_decimal_len override inside the NUMERIC block sets the NUMERIC precision from a formatted string length, which can push precision past 38 and break near-max-precision money-range Decimals that previously worked. Two smaller reliability points below.

Recommendation: Request changes.

Comment thread mssql_python/cursor.py Outdated
if batch_scale > paraminfo.decimalDigits:
paraminfo.decimalDigits = batch_scale
# Ensure columnSize also accommodates the longest string form
# (mixed-sign batches, GH-557).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a SQL_NUMERIC param, columnSize is the numeric precision passed to SQLBindParameter (ddbc_bindings.cpp, 6th arg), not a string length. The max_decimal_len > columnSize override just below counts the decimal point and sign, so it sets precision to batch_precision + 1/2. Now that money-range Decimals flow into this NUMERIC block, a full-scale value like Decimal("1E-38") or Decimal("0."+"1"*38) becomes NUMERIC(40,38) and the driver rejects it (max precision 38), a regression from the prior VARCHAR path and inconsistent with the execute() fix (GH-740). batch_precision already sizes precision correctly; the string-length need is handled separately in the SQL_VARCHAR block below. Please drop this override in the NUMERIC branch and add an executemany test for Decimal("1E-38") into NUMERIC(38,38).

Comment thread mssql_python/cursor.py
# Ensure columnSize accommodates the longest string representation
# One NUMERIC(precision, scale) must fit every Decimal in the
# batch (GH-745). Sample-only precision/scale is not enough.
batch_precision, batch_scale = self._batch_decimal_precision_scale(column)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

batch_precision is applied to columnSize without a <=38 check. The sample-level guard in _map_sql_type doesn't cover the whole batch, so a mixed batch (many integer digits in one row, many fractional in another) can produce batch_precision > 38 even when each value is individually storable, and the driver then fails with an opaque message. Consider raising the existing "precision too high" ValueError when batch_precision > 38. Don't clamp, since that would silently truncate.

Comment thread mssql_python/cursor.py Outdated
# against a smaller numeric column does not overflow. executemany still
# string-binds via SQL_C_CHAR below; setinputsizes DECIMAL stays on the
# GH-503 string path above.
decimal_as_numeric = isinstance(sample_value, decimal.Decimal)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deriving decimal_as_numeric from just the sample type forces the whole column onto the NUMERIC/string-decimal path. A heterogeneous column (Decimal sample plus a stray non-numeric value) that previously bound as VARCHAR will now raise in the Decimal conversion loop below. Consider gating this on "all non-NULL values are Decimal," or documenting heterogeneous columns as unsupported on this path.

…inding

Address review feedback on the executemany SQL_NUMERIC path: keep columnSize as
numeric precision (not string length), raise when batch precision exceeds 38,
force NUMERIC only when every non-NULL value is Decimal, and cover the cases
with unit tests.
@vyrnsynx

Copy link
Copy Markdown
Author

Sumit Sarabhai (@sumitmsft) Thanks for the review — addressed in the latest commit:

  1. Dropped the max_decimal_len override inside the NUMERIC branch so columnSize stays as numeric precision (not string length).
  2. Raise ValueError when batch precision exceeds 38 (no clamp).
  3. Force decimal_as_numeric only when every non-NULL value in the column is a Decimal.

Also added unit coverage for near-max precision (1E-38), batch precision >38, heterogeneous columns, and non-finite Decimals.

@vyrnsynx

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decimal in money range still string-binds on the executemany path

3 participants