Skip to content

FIX: GH-Issue (#532) - Login failures are raised as RuntimeError instead of a mssql_python exception#562

Open
gargsaumya wants to merge 6 commits into
mainfrom
saumya/gh-532
Open

FIX: GH-Issue (#532) - Login failures are raised as RuntimeError instead of a mssql_python exception#562
gargsaumya wants to merge 6 commits into
mainfrom
saumya/gh-532

Conversation

@gargsaumya
Copy link
Copy Markdown
Contributor

@gargsaumya gargsaumya commented May 11, 2026

Work Item / Issue Reference

AB#44632

GitHub Issue: #532


Summary

This pull request improves error handling in the mssql_python connection layer by ensuring that low-level runtime errors from the C++ pybind layer are consistently mapped to the correct DB-API 2.0 exceptions using SQLSTATE codes. This makes error reporting more robust and predictable, especially during connection, commit, and rollback operations. The changes also add comprehensive tests to verify this behavior.

Error handling improvements:

  • Introduced the _raise_connection_error function in connection.py to parse RuntimeError messages from the C++ layer, extract SQLSTATE codes, and map them to appropriate DB-API exceptions using sqlstate_to_exception. If no SQLSTATE is found, a fallback OperationalError is raised. ([mssql_python/connection.pyR61-R89](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R61-R89))
  • Updated the connection initialization, setautocommit, commit, and rollback methods in connection.py to wrap calls to the native layer in try/except blocks that utilize _raise_connection_error, ensuring consistent exception mapping throughout the connection lifecycle. ([[1]](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R362-R367), [[2]](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R528-R531), [[3]](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R1516-R1519), [[4]](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R1542-R1545))
  • Modified the C++ Connection::checkError method to format error messages as "SQLSTATE:XXXXX:<odbc_error_message>", enabling the Python layer to parse and handle errors correctly. ([mssql_python/pybind/connection/connection.cppR182-R186](https://github.com/microsoft/mssql-python/pull/562/files#diff-eca696c13d997f510e5f9b16288ed1deb0ad132768c283eda1518f78edf9b6ecR182-R186))

Testing enhancements:

  • Added a regression test (test_connect_runtime_error_mapped_to_correct_dbapi_exception) to verify that connection, commit, and rollback errors from the C++ layer are mapped to the correct DB-API exceptions, including handling of unknown or missing SQLSTATE codes. ([tests/test_006_exceptions.pyR193-R257](https://github.com/microsoft/mssql-python/pull/562/files#diff-16f96f3facfc774c12b49a09116108763139a8a92ceef526fbb4f4f01600c6fdR193-R257))

Dependency update:

  • Imported sqlstate_to_exception in connection.py to support the new error mapping logic. ([mssql_python/connection.pyR41](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R41))

@github-actions github-actions Bot added the pr-size: medium Moderate update size label May 11, 2026
Comment thread tests/test_006_exceptions.py Fixed
Comment thread tests/test_006_exceptions.py Fixed
Comment thread tests/test_006_exceptions.py Fixed
Comment thread tests/test_006_exceptions.py Fixed
Comment thread tests/test_006_exceptions.py Fixed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

📊 Code Coverage Report

🔥 Diff Coverage

86%


🎯 Overall Coverage

79%


📈 Total Lines Covered: 6937 out of 8761
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/connection.py (89.2%): Missing lines 501-502,544-545
  • mssql_python/pybind/connection/connection.cpp (66.7%): Missing lines 191-192

Summary

  • Total: 43 lines
  • Missing: 6 lines
  • Coverage: 86%

mssql_python/connection.py

Lines 497-506

  497             bool: True if autocommit is enabled, False otherwise.
  498         """
  499         try:
  500             return self._conn.get_autocommit()
! 501         except RuntimeError as e:
! 502             _raise_connection_error(e)
  503 
  504     @autocommit.setter
  505     def autocommit(self, value: bool) -> None:
  506         """

Lines 540-549

  540             DatabaseError: If there is an error while setting the autocommit mode.
  541         """
  542         try:
  543             self._conn.set_autocommit(value)
! 544         except RuntimeError as e:
! 545             _raise_connection_error(e)
  546 
  547     def setencoding(self, encoding: Optional[str] = None, ctype: Optional[int] = None) -> None:
  548         """
  549         Sets the text encoding for SQL statements and text parameters.

mssql_python/pybind/connection/connection.cpp

Lines 187-196

  187         if (sqlState.length() == 5) {
  188             ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg);
  189         } else {
  190             // No valid SQLSTATE (e.g., SQL_INVALID_HANDLE) — throw clean error message
! 191             ThrowStdException(errorMsg);
! 192         }
  193     }
  194 }
  195 
  196 void Connection::commit() {


📋 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: 66.5%
mssql_python.row.py: 70.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 74.3%
mssql_python.__init__.py: 77.3%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 79.6%
mssql_python.connection.py: 85.3%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@gargsaumya gargsaumya marked this pull request as ready for review May 11, 2026 05:38
Copilot AI review requested due to automatic review settings May 11, 2026 05:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request addresses GitHub Issue #532 by ensuring connection-layer failures raised from the native pybind layer as RuntimeError are consistently mapped to the appropriate DB-API 2.0 mssql_python exception types using SQLSTATE codes, improving predictability for scenarios like login/token failures and transaction operations.

Changes:

  • Add Python-side _raise_connection_error() to parse RuntimeError messages with embedded SQLSTATE and raise the mapped DB-API exception (with an OperationalError fallback when no SQLSTATE is present).
  • Wrap native connection initialization, setautocommit(), commit(), and rollback() to route pybind RuntimeError through the new mapping logic.
  • Update native Connection::checkError() to include SQLSTATE in thrown error strings and add a regression test covering connect/commit/rollback mapping behavior.

Reviewed changes

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

File Description
mssql_python/connection.py Adds SQLSTATE parsing + exception mapping helper and uses it to wrap key connection lifecycle calls.
mssql_python/pybind/connection/connection.cpp Prefixes checkError() exceptions with SQLSTATE to enable Python-side parsing/mapping.
tests/test_006_exceptions.py Adds regression coverage for mapping pybind RuntimeError to DB-API exceptions during connect/commit/rollback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mssql_python/pybind/connection/connection.cpp Outdated
Comment thread mssql_python/connection.py Outdated
Comment thread tests/test_006_exceptions.py
gargsaumya added a commit that referenced this pull request May 13, 2026
- C++ checkError(): Only add SQLSTATE prefix when sqlState is exactly 5 chars
  Prevents unparseable messages like 'SQLSTATE::Invalid handle!'
- Python _raise_connection_error(): Handle malformed SQLSTATE gracefully
  Regex now accepts 0-5 chars and validates length before mapping
- Tests: Add coverage for unmapped SQLSTATE and malformed SQLSTATE cases
  Tests now verify DatabaseError for unknown codes and OperationalError for empty codes

Addresses review comments on PR #562
Comment thread tests/test_006_exceptions.py Fixed
Comment thread tests/test_006_exceptions.py Fixed
gargsaumya added a commit that referenced this pull request May 13, 2026
- C++ checkError(): Only add SQLSTATE prefix when sqlState is exactly 5 chars
  Prevents unparseable messages like 'SQLSTATE::Invalid handle!'
- Python _raise_connection_error(): Handle malformed SQLSTATE gracefully
  Regex now accepts 0-5 chars and validates length before mapping
- Tests: Add coverage for unmapped SQLSTATE and malformed SQLSTATE cases
  Tests now verify DatabaseError for unknown codes and OperationalError for empty codes

Addresses review comments on PR #562
gargsaumya added a commit that referenced this pull request May 13, 2026
- Replace all 5 instances of 'localhost' with 'testserver' in mock tests
- These tests use mocked connections and never actually connect
- Addresses GitHub Advanced Security DevSkim notices on PR #562
  (lines 209, 221, 231, 240, 251)
Copy link
Copy Markdown
Collaborator

@bewithgaurav bewithgaurav left a comment

Choose a reason for hiding this comment

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

repro'd #532 on this branch and main (macos): bad-host connect now raises OperationalError with the real ODBC text, main still raises raw RuntimeError. new test passes.

one inline note about the SQLSTATE: prefix leaking through the connection methods that aren't wrapped.

Comment thread mssql_python/connection.py
Copy link
Copy Markdown
Collaborator

@bewithgaurav bewithgaurav left a comment

Choose a reason for hiding this comment

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

requesting changes per the inline follow-up on _raise_connection_error. fine to defer to a separate task if out of scope for #532.

gargsaumya added a commit that referenced this pull request May 13, 2026
Address reviewer feedback: wrap remaining connection methods that call C++
to ensure complete SQLSTATE error mapping coverage.

Additional methods now wrapped with try-catch + _raise_connection_error:
- autocommit property getter (get_autocommit)
- set_attr() for connection attributes
- getinfo() for connection information
- close() internal rollback during cleanup

This ensures users never see raw RuntimeError with SQLSTATE prefixes
from any connection operation. All C++ checkError() calls now properly
map to correct DB-API 2.0 exception types.

Addresses review comment on PR #562
@gargsaumya
Copy link
Copy Markdown
Contributor Author

requesting changes per the inline follow-up on _raise_connection_error. fine to defer to a separate task if out of scope for #532.

Thanks for the review! I've extended exception mapping to the relevant connection methods. getinfo() and set_attr() don't need wrapping because These methods already handle RuntimeError in their existing exception handlers.

Re: C++ exception class approach: I agree with implementing a custom exception hierarchy in C++, I've added a task to the backlog to track this.

@gargsaumya gargsaumya requested a review from bewithgaurav May 14, 2026 02:12
bewithgaurav
bewithgaurav previously approved these changes May 14, 2026
Copy link
Copy Markdown
Collaborator

@bewithgaurav bewithgaurav left a comment

Choose a reason for hiding this comment

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

lgtm

Login failures and other connection-time errors raised by the C++ pybind layer were surfacing as plain RuntimeError instead of a mssql_python exception, making it impossible for callers to catch them via the DB-API 2.0 exception hierarchy.

Connection::checkError() now embeds the SQLSTATE code in the thrown message (SQLSTATE:XXXXX:<msg>) so the Python layer can map it to the correct exception class via sqlstate_to_exception() -- consistent with how cursor-level errors are already handled in helpers.py.

The new _raise_connection_error() helper is applied to all four connection operations that go through checkError(): connect, commit, rollback, and set_autocommit.

Note: when PR #526 (simdutf) merges, the two WideToUTF8() calls in connection.cpp::checkError() will need updating to utf16LeToUtf8Alloc().

Fixes #532
- C++ checkError(): Only add SQLSTATE prefix when sqlState is exactly 5 chars
  Prevents unparseable messages like 'SQLSTATE::Invalid handle!'
- Python _raise_connection_error(): Handle malformed SQLSTATE gracefully
  Regex now accepts 0-5 chars and validates length before mapping
- Tests: Add coverage for unmapped SQLSTATE and malformed SQLSTATE cases
  Tests now verify DatabaseError for unknown codes and OperationalError for empty codes

Addresses review comments on PR #562
- Replace all 5 instances of 'localhost' with 'testserver' in mock tests
- These tests use mocked connections and never actually connect
- Addresses GitHub Advanced Security DevSkim notices on PR #562
  (lines 209, 221, 231, 240, 251)
Address reviewer feedback: wrap remaining connection methods that call C++
to ensure complete SQLSTATE error mapping coverage.

Additional methods now wrapped with try-catch + _raise_connection_error:
- autocommit property getter (get_autocommit)
- set_attr() for connection attributes
- getinfo() for connection information
- close() internal rollback during cleanup

This ensures users never see raw RuntimeError with SQLSTATE prefixes
from any connection operation. All C++ checkError() calls now properly
map to correct DB-API 2.0 exception types.

Addresses review comment on PR #562
Both methods already have their own exception handling:
- getinfo() returns None for invalid info types (graceful fallback)
- set_attr() determines appropriate exception type based on error

Adding RuntimeError wrappers broke existing behavior:
- getinfo() was raising ProgrammingError instead of returning None
- This caused test_comprehensive_getinfo_scenarios to fail

The RuntimeError wrappers in autocommit getter and close() remain
since those methods don't have conflicting exception handling.

Fixes test_comprehensive_getinfo_scenarios failure in CI.
PR #526 changed ErrorInfo to return std::string directly instead of
std::wstring, and removed WideToUTF8() function. Updated checkError()
to use direct assignment since err.sqlState and err.ddbcErrorMsg are
now already std::string.

This fixes compilation errors after rebase on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants