FIX: bind executemany money-range Decimals as SQL_NUMERIC - #752
FIX: bind executemany money-range Decimals as SQL_NUMERIC#752VyrnSynx (vyrnsynx) wants to merge 3 commits into
Conversation
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
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 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 bindDecimalcolumns asSQL_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.
| 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 |
Sumit Sarabhai (sumitmsft)
left a comment
There was a problem hiding this comment.
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.
| if batch_scale > paraminfo.decimalDigits: | ||
| paraminfo.decimalDigits = batch_scale | ||
| # Ensure columnSize also accommodates the longest string form | ||
| # (mixed-sign batches, GH-557). |
There was a problem hiding this comment.
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).
| # 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) |
There was a problem hiding this comment.
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.
| # 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) |
There was a problem hiding this comment.
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.
|
Sumit Sarabhai (@sumitmsft) Thanks for the review — addressed in the latest commit:
Also added unit coverage for near-max precision ( |
|
@microsoft-github-policy-service agree |
Work Item / Issue Reference
Summary
executemanyauto-detect still used the MONEY/SMALLMONEY-range VARCHAR shortcut after #742 fixedexecute(). A money-rangeDecimalcompared against a smaller numeric column could still overflow on that path.This change binds auto-detected Decimal columns as
SQL_NUMERICwith a batch-wide precision/scale (values still go through the existingSQL_C_CHARstring conversion). ThesetinputsizesDECIMAL/NUMERIC string path (GH-503) and mixed-sign VARCHAR sizing (GH-557) are left alone.Fixes #745