Skip to content

FIX: gate SQL_WVARCHAR output-converter fallback to string/binary columns (#691) - #692

Open
jahnvi480 wants to merge 1 commit into
mainfrom
jahnvi/fix-691-wvarchar-catchall
Open

FIX: gate SQL_WVARCHAR output-converter fallback to string/binary columns (#691)#692
jahnvi480 wants to merge 1 commit into
mainfrom
jahnvi/fix-691-wvarchar-catchall

Conversation

@jahnvi480

@jahnvi480 jahnvi480 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#46685

GitHub Issue: #691


Summary

A SQL_WVARCHAR output converter was being applied as an unconditional
catch-all to non-string columns. Cursor._build_converter_map fell back to the
converter registered for SQL_WVARCHAR for any column that had no direct
type-keyed converter, so registering a single SQL_WVARCHAR converter mangled
INT / DECIMAL / DATE values (e.g. 42 came back converted).

This PR gates that legacy fallback on the column's mapped Python type being
str/bytes, mirroring the isinstance(value, (str, bytes)) guard already
present in Row._apply_output_converters. The optimized and fallback apply
paths now behave consistently.

Root cause

sql_type = desc[1] is the column's mapped Python type. The fallback ran
whenever no direct converter matched, regardless of the column type:

if converter is None:
    converter = self.connection.get_output_converter(SQL_WVARCHAR)

Row._apply_output_converters gates the same fallback on
isinstance(value, (str, bytes)), so the two paths disagreed.

Fix

if converter is None and sql_type in (str, bytes):
    converter = self.connection.get_output_converter(ddbc_sql_const.SQL_WVARCHAR.value)

bytes is intentionally included for parity with Row._apply_output_converters
(a SQL_WVARCHAR converter still applies to VARBINARY, which the new test
asserts). Also removed the now-redundant in-loop ConstantsDDBC import.

Test

tests/test_003_connection.py::test_output_converter_wvarchar_not_catchall_for_non_string_columns_gh691
registers only a SQL_WVARCHAR converter and selects INT, DECIMAL, DATE,
NVARCHAR, and VARBINARY columns. It asserts the non-string columns are
untouched, the string/binary columns are converted, and — via a spy — that the
converter is invoked exactly twice (only the str/bytes columns).

The call-count assertion matters: the optimized apply path swallows converter
exceptions, so a plain value.decode() converter raises AttributeError on
int/Decimal/date and is silently swallowed. A value-only assertion would
therefore pass with and without the fix. Verified (via git stash) that the
test fails on the unfixed code and passes once the fallback is gated.

Validation

  • black --check --line-length=100 clean on both files.
  • tests/test_003_connection.py: 151 passed, 2 skipped.
  • tests/test_004_cursor.py: 517 passed, 4 skipped.
  • New test proven to fail without the fix and pass with it.

…umns (#691)

Cursor._build_converter_map applied a converter registered for SQL_WVARCHAR
as an unconditional catch-all to every column that lacked a direct type-keyed
converter, so a lone SQL_WVARCHAR converter mangled INT / DECIMAL / DATE
values. Gate the fallback on the column's mapped Python type being str/bytes,
mirroring the isinstance(value, (str, bytes)) guard already used in
Row._apply_output_converters, so the optimized and fallback apply paths agree.

Also drop the now-redundant in-loop import of ConstantsDDBC (already imported
at module scope as ddbc_sql_const).

Adds a regression test that spies on converter invocations and asserts the
converter fires only on the NVARCHAR / VARBINARY columns (a call-count
contract). This is necessary because the optimized apply path swallows
converter exceptions: a plain value.decode() converter raises AttributeError
on int/Decimal/date and is silently swallowed, so a value-only assertion would
pass with and without the fix. The rewritten test fails on the unfixed code
and passes once the fallback is gated.
Copilot AI review requested due to automatic review settings July 29, 2026 07:43
@github-actions github-actions Bot added the pr-size: small Minimal code update label Jul 29, 2026

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.

Pull request overview

This PR fixes an output-converter dispatch bug in the optimized fetch path: a converter registered for SQL_WVARCHAR was being treated as an unconditional fallback for any column without a direct converter, which could corrupt non-string types (e.g., INT, DECIMAL, DATE). The change gates the legacy SQL_WVARCHAR fallback so it only applies to columns whose mapped Python type is str or bytes, aligning the optimized converter map path with the existing runtime guard behavior.

Changes:

  • Gate the SQL_WVARCHAR fallback in Cursor._build_converter_map() to only apply when desc[1] maps to str/bytes.
  • Add a regression test covering mixed INT/DECIMAL/DATE with NVARCHAR and VARBINARY, including a converter invocation count assertion to detect the previously-masked behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
mssql_python/cursor.py Restricts the legacy SQL_WVARCHAR fallback converter to string/binary-mapped columns during converter map construction.
tests/test_003_connection.py Adds a regression test ensuring SQL_WVARCHAR converters do not act as a catch-all for non-string columns and asserts call count.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

100%


🎯 Overall Coverage

81%


📈 Total Lines Covered: 7067 out of 8675
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (100%)

Summary

  • Total: 2 lines
  • Missing: 0 lines
  • Coverage: 100%

📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 76.3%
mssql_python.__init__.py: 77.3%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.pybind.connection.connection.cpp: 83.7%
mssql_python.connection.py: 84.7%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

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

Labels

pr-size: small Minimal code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants