FIX: GH-Issue (#532) - Login failures are raised as RuntimeError instead of a mssql_python exception#562
FIX: GH-Issue (#532) - Login failures are raised as RuntimeError instead of a mssql_python exception#562gargsaumya wants to merge 6 commits into
Conversation
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql_python/connection.pyLines 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.cppLines 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
|
There was a problem hiding this comment.
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 parseRuntimeErrormessages 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(), androllback()to route pybindRuntimeErrorthrough 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.
- 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
- 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)
bewithgaurav
left a comment
There was a problem hiding this comment.
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.
bewithgaurav
left a comment
There was a problem hiding this comment.
requesting changes per the inline follow-up on _raise_connection_error. fine to defer to a separate task if out of scope for #532.
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
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. |
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.
Work Item / Issue Reference
Summary
This pull request improves error handling in the
mssql_pythonconnection 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:
_raise_connection_errorfunction inconnection.pyto parseRuntimeErrormessages from the C++ layer, extract SQLSTATE codes, and map them to appropriate DB-API exceptions usingsqlstate_to_exception. If no SQLSTATE is found, a fallbackOperationalErroris raised. ([mssql_python/connection.pyR61-R89](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R61-R89))setautocommit,commit, androllbackmethods inconnection.pyto 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))Connection::checkErrormethod 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:
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:
sqlstate_to_exceptioninconnection.pyto support the new error mapping logic. ([mssql_python/connection.pyR41](https://github.com/microsoft/mssql-python/pull/562/files#diff-29bb94de45aae51c23a6426d40133c28e4161e68769e08d046059c7186264e90R41))