1717import warnings
1818from typing import List , Union , Any , Optional , Tuple , Sequence , TYPE_CHECKING , Iterable
1919from mssql_python .constants import ConstantsDDBC as ddbc_sql_const , SQLTypes
20- from mssql_python .helpers import check_error
20+ from mssql_python .helpers import check_error , connstr_to_pycore_params
2121from mssql_python .logging import logger
2222from mssql_python import ddbc_bindings
2323from mssql_python .exceptions import (
@@ -844,7 +844,7 @@ def _reset_inputsizes(self) -> None:
844844 self ._inputsizes = None
845845
846846 def _get_c_type_for_sql_type (self , sql_type : int ) -> int :
847- """Map SQL type to appropriate C type for parameter binding"""
847+ """Map SQL type to appropriate C type for parameter binding. """
848848 sql_to_c_type = {
849849 ddbc_sql_const .SQL_CHAR .value : ddbc_sql_const .SQL_C_CHAR .value ,
850850 ddbc_sql_const .SQL_VARCHAR .value : ddbc_sql_const .SQL_C_CHAR .value ,
@@ -865,9 +865,19 @@ def _get_c_type_for_sql_type(self, sql_type: int) -> int:
865865 ddbc_sql_const .SQL_BINARY .value : ddbc_sql_const .SQL_C_BINARY .value ,
866866 ddbc_sql_const .SQL_VARBINARY .value : ddbc_sql_const .SQL_C_BINARY .value ,
867867 ddbc_sql_const .SQL_LONGVARBINARY .value : ddbc_sql_const .SQL_C_BINARY .value ,
868+ # ODBC 3.x date/time types (reported by ODBC 18 driver)
869+ ddbc_sql_const .SQL_TYPE_DATE .value : ddbc_sql_const .SQL_C_TYPE_DATE .value ,
870+ ddbc_sql_const .SQL_TYPE_TIME .value : ddbc_sql_const .SQL_C_TYPE_TIME .value ,
871+ ddbc_sql_const .SQL_TYPE_TIMESTAMP .value : ddbc_sql_const .SQL_C_TYPE_TIMESTAMP .value ,
872+ ddbc_sql_const .SQL_SS_TIME2 .value : ddbc_sql_const .SQL_C_TYPE_TIME .value ,
873+ ddbc_sql_const .SQL_DATETIMEOFFSET .value : ddbc_sql_const .SQL_C_SS_TIMESTAMPOFFSET .value ,
874+ # ODBC 2.x aliases (accepted by setinputsizes via SQLTypes)
868875 ddbc_sql_const .SQL_DATE .value : ddbc_sql_const .SQL_C_TYPE_DATE .value ,
869876 ddbc_sql_const .SQL_TIME .value : ddbc_sql_const .SQL_C_TYPE_TIME .value ,
870877 ddbc_sql_const .SQL_TIMESTAMP .value : ddbc_sql_const .SQL_C_TYPE_TIMESTAMP .value ,
878+ # Other types
879+ ddbc_sql_const .SQL_GUID .value : ddbc_sql_const .SQL_C_GUID .value ,
880+ ddbc_sql_const .SQL_SS_XML .value : ddbc_sql_const .SQL_C_WCHAR .value ,
871881 }
872882 return sql_to_c_type .get (sql_type , ddbc_sql_const .SQL_C_DEFAULT .value )
873883
@@ -1026,34 +1036,71 @@ def _map_data_type(self, sql_type):
10261036 """
10271037 Map SQL data type to Python data type.
10281038
1039+ Maps the ODBC SQL type code returned by SQLDescribeCol to the
1040+ corresponding Python type for cursor.description[i][1].
1041+
1042+ The ODBC 18 driver for SQL Server reports these type codes:
1043+ Standard ODBC 3.x types:
1044+ SQL_CHAR(1), SQL_VARCHAR(12), SQL_LONGVARCHAR(-1),
1045+ SQL_WCHAR(-8), SQL_WVARCHAR(-9), SQL_WLONGVARCHAR(-10),
1046+ SQL_INTEGER(4), SQL_SMALLINT(5), SQL_TINYINT(-6), SQL_BIGINT(-5),
1047+ SQL_BIT(-7), SQL_FLOAT(6), SQL_REAL(7), SQL_DOUBLE(8),
1048+ SQL_DECIMAL(3), SQL_NUMERIC(2),
1049+ SQL_BINARY(-2), SQL_VARBINARY(-3), SQL_LONGVARBINARY(-4),
1050+ SQL_TYPE_DATE(91), SQL_TYPE_TIME(92), SQL_TYPE_TIMESTAMP(93), SQL_GUID(-11)
1051+ SQL Server-specific types (from msodbcsql.h):
1052+ SQL_SS_TIME2(-154) for time columns
1053+ SQL_DATETIMEOFFSET(-155) for datetimeoffset columns
1054+ SQL_SS_XML(-152) for xml columns
1055+
1056+ ODBC 2.x aliases (9, 10, 11) are also accepted defensively.
1057+
10291058 Args:
1030- sql_type: SQL data type.
1059+ sql_type: SQL data type code from SQLDescribeCol .
10311060
10321061 Returns:
10331062 Corresponding Python data type.
10341063 """
10351064 sql_to_python_type = {
1036- ddbc_sql_const .SQL_INTEGER .value : int ,
1037- ddbc_sql_const .SQL_VARCHAR .value : str ,
1038- ddbc_sql_const .SQL_WVARCHAR .value : str ,
1065+ # String types
10391066 ddbc_sql_const .SQL_CHAR .value : str ,
1067+ ddbc_sql_const .SQL_VARCHAR .value : str ,
1068+ ddbc_sql_const .SQL_LONGVARCHAR .value : str ,
10401069 ddbc_sql_const .SQL_WCHAR .value : str ,
1070+ ddbc_sql_const .SQL_WVARCHAR .value : str ,
1071+ ddbc_sql_const .SQL_WLONGVARCHAR .value : str ,
1072+ # Integer types
1073+ ddbc_sql_const .SQL_INTEGER .value : int ,
1074+ ddbc_sql_const .SQL_SMALLINT .value : int ,
1075+ ddbc_sql_const .SQL_TINYINT .value : int ,
1076+ ddbc_sql_const .SQL_BIGINT .value : int ,
1077+ # Floating-point types
10411078 ddbc_sql_const .SQL_FLOAT .value : float ,
10421079 ddbc_sql_const .SQL_DOUBLE .value : float ,
1080+ ddbc_sql_const .SQL_REAL .value : float ,
1081+ # Exact numeric types
10431082 ddbc_sql_const .SQL_DECIMAL .value : decimal .Decimal ,
10441083 ddbc_sql_const .SQL_NUMERIC .value : decimal .Decimal ,
1045- ddbc_sql_const .SQL_DATE .value : datetime .date ,
1046- ddbc_sql_const .SQL_TIMESTAMP .value : datetime .datetime ,
1047- ddbc_sql_const .SQL_TIME .value : datetime .time ,
1084+ # Date/time types — values the ODBC 18 driver actually reports
1085+ ddbc_sql_const .SQL_TYPE_DATE .value : datetime .date , # 91 — date
1086+ ddbc_sql_const .SQL_TYPE_TIME .value : datetime .time , # 92 — time (ODBC 3.x)
1087+ ddbc_sql_const .SQL_TYPE_TIMESTAMP .value : datetime .datetime , # 93 — datetime/datetime2/smalldatetime
1088+ ddbc_sql_const .SQL_SS_TIME2 .value : datetime .time , # -154 — time
1089+ ddbc_sql_const .SQL_DATETIMEOFFSET .value : datetime .datetime , # -155 — datetimeoffset
1090+ # ODBC 2.x date/time aliases (defensive, in case any driver reports these)
1091+ ddbc_sql_const .SQL_DATE .value : datetime .date , # 9
1092+ ddbc_sql_const .SQL_TIME .value : datetime .time , # 10
1093+ ddbc_sql_const .SQL_TIMESTAMP .value : datetime .datetime , # 11
1094+ # Boolean
10481095 ddbc_sql_const .SQL_BIT .value : bool ,
1049- ddbc_sql_const .SQL_TINYINT .value : int ,
1050- ddbc_sql_const .SQL_SMALLINT .value : int ,
1051- ddbc_sql_const .SQL_BIGINT .value : int ,
1096+ # Binary types
10521097 ddbc_sql_const .SQL_BINARY .value : bytes ,
10531098 ddbc_sql_const .SQL_VARBINARY .value : bytes ,
10541099 ddbc_sql_const .SQL_LONGVARBINARY .value : bytes ,
1100+ # UUID
10551101 ddbc_sql_const .SQL_GUID .value : uuid .UUID ,
1056- # Add more mappings as needed
1102+ # XML — driver reports SQL_SS_XML (-152), fetched as str
1103+ ddbc_sql_const .SQL_SS_XML .value : str ,
10571104 }
10581105 return sql_to_python_type .get (sql_type , str )
10591106
@@ -2451,6 +2498,7 @@ def nextset(self) -> Union[bool, None]:
24512498 )
24522499 return True
24532500
2501+ # ── Mapping from ODBC connection-string keywords (lowercase, as _parse returns)
24542502 def _bulkcopy (
24552503 self ,
24562504 table_name : str ,
@@ -2585,38 +2633,10 @@ def _bulkcopy(
25852633 "Specify the target database explicitly to avoid accidentally writing to system databases."
25862634 )
25872635
2588- # Build connection context for bulk copy library
2589- # Note: Password is extracted separately to avoid storing it in the main context
2590- # dict that could be accidentally logged or exposed in error messages.
2591- trust_cert = params .get ("trustservercertificate" , "yes" ).lower () in ("yes" , "true" )
2592-
2593- # Parse encryption setting from connection string
2594- encrypt_param = params .get ("encrypt" )
2595- if encrypt_param is not None :
2596- encrypt_value = encrypt_param .strip ().lower ()
2597- if encrypt_value in ("yes" , "true" , "mandatory" , "required" ):
2598- encryption = "Required"
2599- elif encrypt_value in ("no" , "false" , "optional" ):
2600- encryption = "Optional"
2601- else :
2602- # Pass through unrecognized values (e.g., "Strict") to the underlying driver
2603- encryption = encrypt_param
2604- else :
2605- encryption = "Optional"
2606-
2607- context = {
2608- "server" : params .get ("server" ),
2609- "database" : params .get ("database" ),
2610- "trust_server_certificate" : trust_cert ,
2611- "encryption" : encryption ,
2612- }
2613-
2614- # Build pycore_context with appropriate authentication.
2615- # For Azure AD: acquire a FRESH token right now instead of reusing
2616- # the one from connect() time — avoids expired-token errors when
2617- # bulkcopy() is called long after the original connection.
2618- pycore_context = dict (context )
2636+ # Translate parsed connection string into the dict py-core expects.
2637+ pycore_context = connstr_to_pycore_params (params )
26192638
2639+ # Token acquisition — only thing cursor must handle (needs azure-identity SDK)
26202640 if self .connection ._auth_type :
26212641 # Fresh token acquisition for mssql-py-core connection
26222642 from mssql_python .auth import AADAuth
@@ -2633,10 +2653,6 @@ def _bulkcopy(
26332653 "Bulk copy: acquired fresh Azure AD token for auth_type=%s" ,
26342654 self .connection ._auth_type ,
26352655 )
2636- else :
2637- # SQL Server authentication — use uid/password from connection string
2638- pycore_context ["user_name" ] = params .get ("uid" , "" )
2639- pycore_context ["password" ] = params .get ("pwd" , "" )
26402656
26412657 pycore_connection = None
26422658 pycore_cursor = None
@@ -2675,9 +2691,8 @@ def _bulkcopy(
26752691 finally :
26762692 # Clear sensitive data to minimize memory exposure
26772693 if pycore_context :
2678- pycore_context .pop ("password" , None )
2679- pycore_context .pop ("user_name" , None )
2680- pycore_context .pop ("access_token" , None )
2694+ for key in ("password" , "user_name" , "access_token" ):
2695+ pycore_context .pop (key , None )
26812696 # Clean up bulk copy resources
26822697 for resource in (pycore_cursor , pycore_connection ):
26832698 if resource and hasattr (resource , "close" ):
0 commit comments