From 969863002ff5d22a7217df5e145665e5c3117973 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 11 May 2026 10:24:42 +0530 Subject: [PATCH 1/6] FIX: Map connection RuntimeError to correct DB-API 2.0 exception (#532) 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:) 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 --- mssql_python/connection.py | 54 +++++++++++++-- mssql_python/pybind/connection/connection.cpp | 5 +- tests/test_006_exceptions.py | 65 +++++++++++++++++++ 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/mssql_python/connection.py b/mssql_python/connection.py index 0064917aa..d3c2579b7 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -38,6 +38,7 @@ InternalError, ProgrammingError, NotSupportedError, + sqlstate_to_exception, ) from mssql_python.auth import extract_auth_type, process_connection_string from mssql_python.constants import ConstantsDDBC, GetInfoConstants @@ -57,6 +58,35 @@ # Note: "utf-16" with BOM is NOT included as it's problematic for SQL_WCHAR UTF16_ENCODINGS: frozenset[str] = frozenset(["utf-16le", "utf-16be"]) +_SQLSTATE_RE = re.compile(r"^SQLSTATE:([A-Z0-9]{5}):(.*)", re.DOTALL) + + +def _raise_connection_error(e: RuntimeError) -> None: + """Map a RuntimeError from the C++ pybind layer to the correct DB-API 2.0 exception. + + Connection::checkError() throws "SQLSTATE:XXXXX:" so the SQLSTATE + can be mapped via sqlstate_to_exception(), consistent with cursor-level error handling. + """ + error_msg = str(e) + match = _SQLSTATE_RE.match(error_msg) + if match: + sqlstate, ddbc_error = match.group(1), match.group(2) + exc = sqlstate_to_exception(sqlstate, ddbc_error) + if exc is None: + logger.error("Unknown SQLSTATE %s, raising DatabaseError", sqlstate) + raise DatabaseError( + driver_error=f"An error occurred with SQLSTATE code: {sqlstate}", + ddbc_error=ddbc_error, + ) from None + logger.error("Connection error (SQLSTATE %s): %s", sqlstate, ddbc_error) + raise exc from None + # Fallback: no SQLSTATE prefix — e.g. "Connection handle not allocated" + logger.error("Connection error: %s", error_msg) + raise OperationalError( + driver_error="Connection operation failed", + ddbc_error=error_msg, + ) from None + def _validate_utf16_wchar_compatibility( encoding: str, wchar_type: int, context: str = "SQL_WCHAR" @@ -333,9 +363,12 @@ def __init__( if not PoolingManager.is_initialized(): PoolingManager.enable() self._pooling = PoolingManager.is_enabled() - self._conn = ddbc_bindings.Connection( - self.connection_str, self._pooling, self._attrs_before - ) + try: + self._conn = ddbc_bindings.Connection( + self.connection_str, self._pooling, self._attrs_before + ) + except RuntimeError as e: + _raise_connection_error(e) self.setautocommit(autocommit) # Register this connection for cleanup before Python shutdown @@ -496,7 +529,10 @@ def setautocommit(self, value: bool = False) -> None: Raises: DatabaseError: If there is an error while setting the autocommit mode. """ - self._conn.set_autocommit(value) + try: + self._conn.set_autocommit(value) + except RuntimeError as e: + _raise_connection_error(e) def setencoding(self, encoding: Optional[str] = None, ctype: Optional[int] = None) -> None: """ @@ -1491,7 +1527,10 @@ def commit(self) -> None: ) # Commit the current transaction - self._conn.commit() + try: + self._conn.commit() + except RuntimeError as e: + _raise_connection_error(e) logger.info("Transaction committed successfully.") def rollback(self) -> None: @@ -1514,7 +1553,10 @@ def rollback(self) -> None: ) # Roll back the current transaction - self._conn.rollback() + try: + self._conn.rollback() + except RuntimeError as e: + _raise_connection_error(e) logger.info("Transaction rolled back successfully.") def close(self) -> None: diff --git a/mssql_python/pybind/connection/connection.cpp b/mssql_python/pybind/connection/connection.cpp index c70de7b09..5de02cdec 100644 --- a/mssql_python/pybind/connection/connection.cpp +++ b/mssql_python/pybind/connection/connection.cpp @@ -170,8 +170,11 @@ void Connection::disconnect() { // DB spec compliant void Connection::checkError(SQLRETURN ret) const { if (!SQL_SUCCEEDED(ret)) { + // Format: "SQLSTATE:XXXXX:" — parsed by _raise_connection_error() ErrorInfo err = SQLCheckError_Wrap(SQL_HANDLE_DBC, _dbcHandle, ret); - ThrowStdException(err.ddbcErrorMsg); + std::string sqlState = WideToUTF8(err.sqlState); + std::string errorMsg = WideToUTF8(err.ddbcErrorMsg); + ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg); } } diff --git a/tests/test_006_exceptions.py b/tests/test_006_exceptions.py index 37c0c1285..f68ee5781 100644 --- a/tests/test_006_exceptions.py +++ b/tests/test_006_exceptions.py @@ -190,6 +190,71 @@ def test_connection_error(): assert "Incomplete specification" in str(excinfo.value) or "has no value" in str(excinfo.value) +def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): + """Regression test for https://github.com/microsoft/mssql-python/issues/532. + + Connection failures from the C++ pybind layer (RuntimeError) must be mapped + to the correct DB-API 2.0 exception via the embedded SQLSTATE code. + This covers connect(), commit(), and rollback() — all of which go through + Connection::checkError() in the C++ layer. + """ + from unittest.mock import MagicMock, patch + + # SQLSTATE 28000 = login failed -> OperationalError + with patch( + "mssql_python.connection.ddbc_bindings.Connection", + 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;") + assert "Login failed for user" in exc_info.value.ddbc_error + assert not isinstance(exc_info.value, RuntimeError) + + # SQLSTATE IM002 = driver not found -> OperationalError (per SQLSTATE mapping) + with patch( + "mssql_python.connection.ddbc_bindings.Connection", + side_effect=RuntimeError( + "SQLSTATE:IM002:Data source name not found and no default driver specified" + ), + ): + with pytest.raises(OperationalError) as exc_info: + 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) + + # No SQLSTATE prefix -> fallback OperationalError + with patch( + "mssql_python.connection.ddbc_bindings.Connection", + side_effect=RuntimeError("Connection handle not allocated"), + ): + with pytest.raises(OperationalError) as exc_info: + connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + assert "Connection handle not allocated" in exc_info.value.ddbc_error + assert not isinstance(exc_info.value, RuntimeError) + + # commit() failure -> OperationalError via same _raise_connection_error path + mock_conn = MagicMock() + 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;") + with pytest.raises(OperationalError) as exc_info: + conn.commit() + assert "Communication link failure" in exc_info.value.ddbc_error + assert not isinstance(exc_info.value, RuntimeError) + + # rollback() failure -> OperationalError via same _raise_connection_error path + mock_conn2 = MagicMock() + 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;") + with pytest.raises(OperationalError) as exc_info: + conn2.rollback() + assert "Communication link failure" in exc_info.value.ddbc_error + assert not isinstance(exc_info.value, RuntimeError) + + def test_truncate_error_message_successful_cases(): """Test truncate_error_message with valid Microsoft messages for comparison.""" From 583c78990a6c64a29cf7b0371d40f570551287a7 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 13 May 2026 10:46:09 +0530 Subject: [PATCH 2/6] Address PR review comments: fix SQLSTATE handling and test coverage - 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 --- mssql_python/connection.py | 9 +++++++- mssql_python/pybind/connection/connection.cpp | 8 ++++++- tests/test_006_exceptions.py | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/mssql_python/connection.py b/mssql_python/connection.py index d3c2579b7..06df4f702 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -58,7 +58,7 @@ # Note: "utf-16" with BOM is NOT included as it's problematic for SQL_WCHAR UTF16_ENCODINGS: frozenset[str] = frozenset(["utf-16le", "utf-16be"]) -_SQLSTATE_RE = re.compile(r"^SQLSTATE:([A-Z0-9]{5}):(.*)", re.DOTALL) +_SQLSTATE_RE = re.compile(r"^SQLSTATE:([A-Z0-9]{0,5}):(.*)", re.DOTALL) def _raise_connection_error(e: RuntimeError) -> None: @@ -71,6 +71,13 @@ def _raise_connection_error(e: RuntimeError) -> None: match = _SQLSTATE_RE.match(error_msg) if match: sqlstate, ddbc_error = match.group(1), match.group(2) + # Handle malformed SQLSTATE prefix (empty or invalid code) + if not sqlstate or len(sqlstate) != 5: + logger.error("Connection error (malformed SQLSTATE): %s", ddbc_error) + raise OperationalError( + driver_error="Connection operation failed", + ddbc_error=ddbc_error, + ) from None exc = sqlstate_to_exception(sqlstate, ddbc_error) if exc is None: logger.error("Unknown SQLSTATE %s, raising DatabaseError", sqlstate) diff --git a/mssql_python/pybind/connection/connection.cpp b/mssql_python/pybind/connection/connection.cpp index 5de02cdec..8d79b364c 100644 --- a/mssql_python/pybind/connection/connection.cpp +++ b/mssql_python/pybind/connection/connection.cpp @@ -174,7 +174,13 @@ void Connection::checkError(SQLRETURN ret) const { ErrorInfo err = SQLCheckError_Wrap(SQL_HANDLE_DBC, _dbcHandle, ret); std::string sqlState = WideToUTF8(err.sqlState); std::string errorMsg = WideToUTF8(err.ddbcErrorMsg); - ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg); + // Only add SQLSTATE prefix if we have a valid 5-character code + if (sqlState.length() == 5) { + ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg); + } else { + // No valid SQLSTATE (e.g., SQL_INVALID_HANDLE) — throw clean error message + ThrowStdException(errorMsg); + } } } diff --git a/tests/test_006_exceptions.py b/tests/test_006_exceptions.py index f68ee5781..9b37243e0 100644 --- a/tests/test_006_exceptions.py +++ b/tests/test_006_exceptions.py @@ -232,6 +232,27 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): assert "Connection handle not allocated" in exc_info.value.ddbc_error assert not isinstance(exc_info.value, RuntimeError) + # Unmapped SQLSTATE (sqlstate_to_exception returns None) -> DatabaseError + with patch( + "mssql_python.connection.ddbc_bindings.Connection", + side_effect=RuntimeError("SQLSTATE:99999:Unknown error with unmapped code"), + ): + with pytest.raises(DatabaseError) as exc_info: + connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + assert "Unknown error with unmapped code" in exc_info.value.ddbc_error + assert "SQLSTATE code: 99999" in exc_info.value.driver_error + assert not isinstance(exc_info.value, RuntimeError) + + # Malformed SQLSTATE (empty code) -> OperationalError + with patch( + "mssql_python.connection.ddbc_bindings.Connection", + side_effect=RuntimeError("SQLSTATE::Invalid handle!"), + ): + with pytest.raises(OperationalError) as exc_info: + connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + assert "Invalid handle!" in exc_info.value.ddbc_error + assert not isinstance(exc_info.value, RuntimeError) + # commit() failure -> OperationalError via same _raise_connection_error path mock_conn = MagicMock() mock_conn.commit.side_effect = RuntimeError("SQLSTATE:08S01:Communication link failure") From e2a51521b56338056c2d4c0fec0592132b677649 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 13 May 2026 10:55:59 +0530 Subject: [PATCH 3/6] Fix DevSkim warnings: Replace localhost with testserver in test - 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) --- tests/test_006_exceptions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_006_exceptions.py b/tests/test_006_exceptions.py index 9b37243e0..868ada4ae 100644 --- a/tests/test_006_exceptions.py +++ b/tests/test_006_exceptions.py @@ -206,7 +206,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): 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;") + connect("Server=testserver;Database=mydb;UID=baduser;PWD=wrongpassword;") assert "Login failed for user" in exc_info.value.ddbc_error assert not isinstance(exc_info.value, RuntimeError) @@ -218,7 +218,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): ), ): with pytest.raises(OperationalError) as exc_info: - connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + connect("Server=testserver;Database=mydb;UID=u;PWD=p;") assert "no default driver" in exc_info.value.ddbc_error assert not isinstance(exc_info.value, RuntimeError) @@ -228,7 +228,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): side_effect=RuntimeError("Connection handle not allocated"), ): with pytest.raises(OperationalError) as exc_info: - connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + connect("Server=testserver;Database=mydb;UID=u;PWD=p;") assert "Connection handle not allocated" in exc_info.value.ddbc_error assert not isinstance(exc_info.value, RuntimeError) @@ -238,7 +238,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): side_effect=RuntimeError("SQLSTATE:99999:Unknown error with unmapped code"), ): with pytest.raises(DatabaseError) as exc_info: - connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + connect("Server=testserver;Database=mydb;UID=u;PWD=p;") assert "Unknown error with unmapped code" in exc_info.value.ddbc_error assert "SQLSTATE code: 99999" in exc_info.value.driver_error assert not isinstance(exc_info.value, RuntimeError) @@ -249,7 +249,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): side_effect=RuntimeError("SQLSTATE::Invalid handle!"), ): with pytest.raises(OperationalError) as exc_info: - connect("Server=localhost;Database=mydb;UID=u;PWD=p;") + connect("Server=testserver;Database=mydb;UID=u;PWD=p;") assert "Invalid handle!" in exc_info.value.ddbc_error assert not isinstance(exc_info.value, RuntimeError) @@ -258,7 +258,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): 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;") + conn = connect("Server=testserver;Database=mydb;UID=u;PWD=p;") with pytest.raises(OperationalError) as exc_info: conn.commit() assert "Communication link failure" in exc_info.value.ddbc_error @@ -269,7 +269,7 @@ def test_connect_runtime_error_mapped_to_correct_dbapi_exception(): 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;") + conn2 = connect("Server=testserver;Database=mydb;UID=u;PWD=p;") with pytest.raises(OperationalError) as exc_info: conn2.rollback() assert "Communication link failure" in exc_info.value.ddbc_error From 3c6e129fb773e4695aea9c5a0a2a8325fab5692e Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 13 May 2026 16:43:30 +0530 Subject: [PATCH 4/6] Extend exception mapping to all connection methods 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 --- mssql_python/connection.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/mssql_python/connection.py b/mssql_python/connection.py index 06df4f702..c6b210425 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -496,7 +496,10 @@ def autocommit(self) -> bool: Returns: bool: True if autocommit is enabled, False otherwise. """ - return self._conn.get_autocommit() + try: + return self._conn.get_autocommit() + except RuntimeError as e: + _raise_connection_error(e) @autocommit.setter def autocommit(self, value: bool) -> None: @@ -928,6 +931,9 @@ def set_attr(self, attribute: int, value: Union[int, str, bytes, bytearray]) -> self._conn.set_attr(attribute, value) logger.info(f"Connection attribute {sanitized_attr} set successfully") + except RuntimeError as e: + # Handle C++ layer RuntimeError with proper DB-API exception mapping + _raise_connection_error(e) except Exception as e: error_msg = f"Failed to set connection attribute {sanitized_attr}: {str(e)}" @@ -1308,6 +1314,9 @@ def getinfo(self, info_type: int) -> Union[str, int, bool, None]: # Get the raw result from the C++ layer try: raw_result = self._conn.get_info(info_type) + except RuntimeError as e: + # Handle C++ layer RuntimeError with proper DB-API exception mapping + _raise_connection_error(e) except Exception as e: # pylint: disable=broad-exception-caught # Log the error and return None for invalid info types logger.warning(f"getinfo({info_type}) failed: {e}") @@ -1619,7 +1628,11 @@ def close(self) -> None: # For autocommit True, this is not necessary as each statement is # committed immediately logger.debug("Rolling back uncommitted changes before closing connection.") - self._conn.rollback() + try: + self._conn.rollback() + except RuntimeError as e: + # Handle C++ layer RuntimeError with proper DB-API exception mapping + _raise_connection_error(e) # TODO: Check potential race conditions in case of multithreaded scenarios # Close the connection self._conn.close() From d9890304a8c7fcab7a96a1949a40dbdd1745a510 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Thu, 14 May 2026 07:30:56 +0530 Subject: [PATCH 5/6] FIX: Remove exception wrappers from getinfo() and set_attr() 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. --- mssql_python/connection.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mssql_python/connection.py b/mssql_python/connection.py index c6b210425..83457650c 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -931,9 +931,6 @@ def set_attr(self, attribute: int, value: Union[int, str, bytes, bytearray]) -> self._conn.set_attr(attribute, value) logger.info(f"Connection attribute {sanitized_attr} set successfully") - except RuntimeError as e: - # Handle C++ layer RuntimeError with proper DB-API exception mapping - _raise_connection_error(e) except Exception as e: error_msg = f"Failed to set connection attribute {sanitized_attr}: {str(e)}" @@ -1314,9 +1311,6 @@ def getinfo(self, info_type: int) -> Union[str, int, bool, None]: # Get the raw result from the C++ layer try: raw_result = self._conn.get_info(info_type) - except RuntimeError as e: - # Handle C++ layer RuntimeError with proper DB-API exception mapping - _raise_connection_error(e) except Exception as e: # pylint: disable=broad-exception-caught # Log the error and return None for invalid info types logger.warning(f"getinfo({info_type}) failed: {e}") From 42f18e5e7a0eedc0b0a5197d470f68725916e504 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Thu, 14 May 2026 16:53:24 +0530 Subject: [PATCH 6/6] FIX: Adapt to PR #526 utf16 refactor - remove WideToUTF8 calls 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. --- mssql_python/pybind/connection/connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mssql_python/pybind/connection/connection.cpp b/mssql_python/pybind/connection/connection.cpp index 8d79b364c..8f9e0a5e3 100644 --- a/mssql_python/pybind/connection/connection.cpp +++ b/mssql_python/pybind/connection/connection.cpp @@ -172,8 +172,8 @@ void Connection::checkError(SQLRETURN ret) const { if (!SQL_SUCCEEDED(ret)) { // Format: "SQLSTATE:XXXXX:" — 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); + std::string sqlState = err.sqlState; + std::string errorMsg = err.ddbcErrorMsg; // Only add SQLSTATE prefix if we have a valid 5-character code if (sqlState.length() == 5) { ThrowStdException("SQLSTATE:" + sqlState + ":" + errorMsg);