3838 InternalError ,
3939 ProgrammingError ,
4040 NotSupportedError ,
41+ sqlstate_to_exception ,
4142)
4243from mssql_python .auth import extract_auth_type , process_connection_string
4344from mssql_python .constants import ConstantsDDBC , GetInfoConstants
5758# Note: "utf-16" with BOM is NOT included as it's problematic for SQL_WCHAR
5859UTF16_ENCODINGS : frozenset [str ] = frozenset (["utf-16le" , "utf-16be" ])
5960
61+ _SQLSTATE_RE = re .compile (r"^SQLSTATE:([A-Z0-9]{0,5}):(.*)" , re .DOTALL )
62+
63+
64+ def _raise_connection_error (e : RuntimeError ) -> None :
65+ """Map a RuntimeError from the C++ pybind layer to the correct DB-API 2.0 exception.
66+
67+ Connection::checkError() throws "SQLSTATE:XXXXX:<odbc_message>" so the SQLSTATE
68+ can be mapped via sqlstate_to_exception(), consistent with cursor-level error handling.
69+ """
70+ error_msg = str (e )
71+ match = _SQLSTATE_RE .match (error_msg )
72+ if match :
73+ sqlstate , ddbc_error = match .group (1 ), match .group (2 )
74+ # Handle malformed SQLSTATE prefix (empty or invalid code)
75+ if not sqlstate or len (sqlstate ) != 5 :
76+ logger .error ("Connection error (malformed SQLSTATE): %s" , ddbc_error )
77+ raise OperationalError (
78+ driver_error = "Connection operation failed" ,
79+ ddbc_error = ddbc_error ,
80+ ) from None
81+ exc = sqlstate_to_exception (sqlstate , ddbc_error )
82+ if exc is None :
83+ logger .error ("Unknown SQLSTATE %s, raising DatabaseError" , sqlstate )
84+ raise DatabaseError (
85+ driver_error = f"An error occurred with SQLSTATE code: { sqlstate } " ,
86+ ddbc_error = ddbc_error ,
87+ ) from None
88+ logger .error ("Connection error (SQLSTATE %s): %s" , sqlstate , ddbc_error )
89+ raise exc from None
90+ # Fallback: no SQLSTATE prefix — e.g. "Connection handle not allocated"
91+ logger .error ("Connection error: %s" , error_msg )
92+ raise OperationalError (
93+ driver_error = "Connection operation failed" ,
94+ ddbc_error = error_msg ,
95+ ) from None
96+
6097
6198def _validate_utf16_wchar_compatibility (
6299 encoding : str , wchar_type : int , context : str = "SQL_WCHAR"
@@ -261,10 +298,14 @@ def __init__(
261298 }
262299
263300 # Initialize decoding settings with Python 3 defaults
301+ # SQL_CHAR default uses SQL_WCHAR ctype so the ODBC driver returns
302+ # UTF-16 data for VARCHAR columns. This avoids encoding mismatches on
303+ # Windows where the driver returns raw bytes in the server's native
304+ # code page (e.g. CP-1252) that may fail to decode as UTF-8.
264305 self ._decoding_settings = {
265306 ConstantsDDBC .SQL_CHAR .value : {
266- "encoding" : "utf-8 " ,
267- "ctype" : ConstantsDDBC .SQL_CHAR .value ,
307+ "encoding" : "utf-16le " ,
308+ "ctype" : ConstantsDDBC .SQL_WCHAR .value ,
268309 },
269310 ConstantsDDBC .SQL_WCHAR .value : {
270311 "encoding" : "utf-16le" ,
@@ -329,9 +370,12 @@ def __init__(
329370 if not PoolingManager .is_initialized ():
330371 PoolingManager .enable ()
331372 self ._pooling = PoolingManager .is_enabled ()
332- self ._conn = ddbc_bindings .Connection (
333- self .connection_str , self ._pooling , self ._attrs_before
334- )
373+ try :
374+ self ._conn = ddbc_bindings .Connection (
375+ self .connection_str , self ._pooling , self ._attrs_before
376+ )
377+ except RuntimeError as e :
378+ _raise_connection_error (e )
335379 self .setautocommit (autocommit )
336380
337381 # Register this connection for cleanup before Python shutdown
@@ -452,7 +496,10 @@ def autocommit(self) -> bool:
452496 Returns:
453497 bool: True if autocommit is enabled, False otherwise.
454498 """
455- return self ._conn .get_autocommit ()
499+ try :
500+ return self ._conn .get_autocommit ()
501+ except RuntimeError as e :
502+ _raise_connection_error (e )
456503
457504 @autocommit .setter
458505 def autocommit (self , value : bool ) -> None :
@@ -492,7 +539,10 @@ def setautocommit(self, value: bool = False) -> None:
492539 Raises:
493540 DatabaseError: If there is an error while setting the autocommit mode.
494541 """
495- self ._conn .set_autocommit (value )
542+ try :
543+ self ._conn .set_autocommit (value )
544+ except RuntimeError as e :
545+ _raise_connection_error (e )
496546
497547 def setencoding (self , encoding : Optional [str ] = None , ctype : Optional [int ] = None ) -> None :
498548 """
@@ -643,9 +693,13 @@ def setdecoding(
643693 sqltype (int): The SQL type being configured: SQL_CHAR, SQL_WCHAR, or SQL_WMETADATA.
644694 SQL_WMETADATA is a special flag for configuring column name decoding.
645695 encoding (str, optional): The Python encoding to use when decoding the data.
646- If None, uses default encoding based on sqltype.
696+ If None, defaults to ``'utf-16le'`` for all sqltypes (SQL_CHAR,
697+ SQL_WCHAR, and SQL_WMETADATA), matching the connection-level
698+ defaults set in ``Connection.__init__``. Passing ``encoding=None``
699+ therefore resets the sqltype to its initial default.
647700 ctype (int, optional): The C data type to request from SQLGetData:
648- SQL_CHAR or SQL_WCHAR. If None, uses default based on encoding.
701+ SQL_CHAR or SQL_WCHAR. If None, uses default based on encoding
702+ (SQL_WCHAR for UTF-16 variants, SQL_CHAR otherwise).
649703
650704 Returns:
651705 None
@@ -655,7 +709,10 @@ def setdecoding(
655709 InterfaceError: If the connection is closed.
656710
657711 Example:
658- # Configure SQL_CHAR to use UTF-8 decoding
712+ # Reset SQL_CHAR to the connection default (utf-16le + SQL_WCHAR ctype)
713+ cnxn.setdecoding(mssql_python.SQL_CHAR)
714+
715+ # Configure SQL_CHAR to use UTF-8 decoding (opt-in, non-default)
659716 cnxn.setdecoding(mssql_python.SQL_CHAR, encoding='utf-8')
660717
661718 # Configure column metadata decoding
@@ -691,12 +748,15 @@ def setdecoding(
691748 ),
692749 )
693750
694- # Set default encoding based on sqltype if not provided
751+ # Set default encoding based on sqltype if not provided.
752+ # All sqltypes default to UTF-16LE to match Connection.__init__ defaults.
753+ # SQL_CHAR uses utf-16le + SQL_WCHAR ctype so the ODBC driver returns
754+ # UTF-16 data for VARCHAR columns, avoiding encoding mismatches on
755+ # Windows where the driver may otherwise return raw bytes in the
756+ # server's native code page (e.g. CP-1252). This makes
757+ # ``setdecoding(SQL_CHAR)`` with no arguments a true reset-to-defaults.
695758 if encoding is None :
696- if sqltype == ConstantsDDBC .SQL_CHAR .value :
697- encoding = "utf-8" # Default for SQL_CHAR in Python 3
698- else : # SQL_WCHAR or SQL_WMETADATA
699- encoding = "utf-16le" # Default for SQL_WCHAR in Python 3
759+ encoding = "utf-16le"
700760
701761 # Validate encoding using cached validation for better performance
702762 if not _validate_encoding (encoding ):
@@ -1477,7 +1537,10 @@ def commit(self) -> None:
14771537 )
14781538
14791539 # Commit the current transaction
1480- self ._conn .commit ()
1540+ try :
1541+ self ._conn .commit ()
1542+ except RuntimeError as e :
1543+ _raise_connection_error (e )
14811544 logger .info ("Transaction committed successfully." )
14821545
14831546 def rollback (self ) -> None :
@@ -1500,7 +1563,10 @@ def rollback(self) -> None:
15001563 )
15011564
15021565 # Roll back the current transaction
1503- self ._conn .rollback ()
1566+ try :
1567+ self ._conn .rollback ()
1568+ except RuntimeError as e :
1569+ _raise_connection_error (e )
15041570 logger .info ("Transaction rolled back successfully." )
15051571
15061572 def close (self ) -> None :
@@ -1556,7 +1622,11 @@ def close(self) -> None:
15561622 # For autocommit True, this is not necessary as each statement is
15571623 # committed immediately
15581624 logger .debug ("Rolling back uncommitted changes before closing connection." )
1559- self ._conn .rollback ()
1625+ try :
1626+ self ._conn .rollback ()
1627+ except RuntimeError as e :
1628+ # Handle C++ layer RuntimeError with proper DB-API exception mapping
1629+ _raise_connection_error (e )
15601630 # TODO: Check potential race conditions in case of multithreaded scenarios
15611631 # Close the connection
15621632 self ._conn .close ()
0 commit comments