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 1 commit 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 1 commit 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))

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
@github-actions github-actions Bot added the pr-size: medium Moderate update size label May 11, 2026
side_effect=RuntimeError("SQLSTATE:28000:Login failed for user 'baduser'."),
):
with pytest.raises(OperationalError) as exc_info:
connect("Server=localhost;Database=mydb;UID=baduser;PWD=wrongpassword;")
),
):
with pytest.raises(OperationalError) as exc_info:
connect("Server=localhost;Database=mydb;UID=u;PWD=p;")
side_effect=RuntimeError("Connection handle not allocated"),
):
with pytest.raises(OperationalError) as exc_info:
connect("Server=localhost;Database=mydb;UID=u;PWD=p;")
mock_conn.commit.side_effect = RuntimeError("SQLSTATE:08S01:Communication link failure")
mock_conn.get_autocommit.return_value = False
with patch("mssql_python.connection.ddbc_bindings.Connection", return_value=mock_conn):
conn = connect("Server=localhost;Database=mydb;UID=u;PWD=p;")
mock_conn2.rollback.side_effect = RuntimeError("SQLSTATE:08S01:Communication link failure")
mock_conn2.get_autocommit.return_value = False
with patch("mssql_python.connection.ddbc_bindings.Connection", return_value=mock_conn2):
conn2 = connect("Server=localhost;Database=mydb;UID=u;PWD=p;")
@github-actions
Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

89%


🎯 Overall Coverage

79%


📈 Total Lines Covered: 6881 out of 8660
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/connection.py (88.9%): Missing lines 77,530-531
  • mssql_python/pybind/connection/connection.cpp (100%)

Summary

  • Total: 29 lines
  • Missing: 3 lines
  • Coverage: 89%

mssql_python/connection.py

Lines 73-81

  73         sqlstate, ddbc_error = match.group(1), match.group(2)
  74         exc = sqlstate_to_exception(sqlstate, ddbc_error)
  75         if exc is None:
  76             logger.error("Unknown SQLSTATE %s, raising DatabaseError", sqlstate)
! 77             raise DatabaseError(
  78                 driver_error=f"An error occurred with SQLSTATE code: {sqlstate}",
  79                 ddbc_error=ddbc_error,
  80             ) from None
  81         logger.error("Connection error (SQLSTATE %s): %s", sqlstate, ddbc_error)

Lines 526-535

  526             DatabaseError: If there is an error while setting the autocommit mode.
  527         """
  528         try:
  529             self._conn.set_autocommit(value)
! 530         except RuntimeError as e:
! 531             _raise_connection_error(e)
  532 
  533     def setencoding(self, encoding: Optional[str] = None, ctype: Optional[int] = None) -> None:
  534         """
  535         Sets the text encoding for SQL statements and text parameters.


📋 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: 67.9%
mssql_python.row.py: 70.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 74.6%
mssql_python.pybind.connection.connection.cpp: 76.2%
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%

🔗 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 on lines +182 to +186
// Format: "SQLSTATE:XXXXX:<odbc_error_message>" — parsed by _raise_connection_error()
ErrorInfo err = SQLCheckError_Wrap(SQL_HANDLE_DBC, _dbcHandle, ret);
std::string sqlState = WideToUTF8(err.sqlState);
std::string errorMsg = WideToUTF8(err.ddbcErrorMsg);
ThrowStdException(errorMsg);
ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg);
Comment on lines +61 to +62
_SQLSTATE_RE = re.compile(r"^SQLSTATE:([A-Z0-9]{5}):(.*)", re.DOTALL)

connect("Server=localhost;Database=mydb;UID=u;PWD=p;")
assert "no default driver" in exc_info.value.ddbc_error
assert not isinstance(exc_info.value, RuntimeError)

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.

3 participants