@@ -1557,3 +1557,129 @@ def test_mssql_pymssql_connection_factory():
15571557 # Clean up the mock module
15581558 if "pymssql" in sys .modules :
15591559 del sys .modules ["pymssql" ]
1560+
1561+
1562+ def test_mssql_pyodbc_connection_datetimeoffset_handling ():
1563+ """Test that the MSSQL pyodbc connection properly handles DATETIMEOFFSET conversion."""
1564+ from datetime import datetime , timezone , timedelta
1565+ import struct
1566+ from unittest .mock import Mock , patch
1567+
1568+ with patch ("pyodbc.connect" ) as mock_pyodbc_connect :
1569+ # Track calls to add_output_converter
1570+ converter_calls = []
1571+
1572+ def mock_add_output_converter (sql_type , converter_func ):
1573+ converter_calls .append ((sql_type , converter_func ))
1574+
1575+ # Create a mock connection that will be returned by pyodbc.connect
1576+ mock_connection = Mock ()
1577+ mock_connection .add_output_converter = mock_add_output_converter
1578+ mock_pyodbc_connect .return_value = mock_connection
1579+
1580+ config = MSSQLConnectionConfig (
1581+ host = "localhost" ,
1582+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1583+ check_import = False ,
1584+ )
1585+
1586+ # Get the connection factory and call it
1587+ factory_with_kwargs = config ._connection_factory_with_kwargs
1588+ connection = factory_with_kwargs ()
1589+
1590+ # Verify that add_output_converter was called for SQL type -155 (DATETIMEOFFSET)
1591+ assert len (converter_calls ) == 1
1592+ sql_type , converter_func = converter_calls [0 ]
1593+ assert sql_type == - 155
1594+
1595+ # Test the converter function with actual DATETIMEOFFSET binary data
1596+ # Create a test DATETIMEOFFSET value: 2023-12-25 15:30:45.123456789 +05:30
1597+ year , month , day = 2023 , 12 , 25
1598+ hour , minute , second = 15 , 30 , 45
1599+ nanoseconds = 123456789
1600+ tz_hour_offset , tz_minute_offset = 5 , 30
1601+
1602+ # Pack the binary data according to the DATETIMEOFFSET format
1603+ binary_data = struct .pack (
1604+ "<6hI2h" ,
1605+ year ,
1606+ month ,
1607+ day ,
1608+ hour ,
1609+ minute ,
1610+ second ,
1611+ nanoseconds ,
1612+ tz_hour_offset ,
1613+ tz_minute_offset ,
1614+ )
1615+
1616+ # Convert using the registered converter
1617+ result = converter_func (binary_data )
1618+
1619+ # Verify the result
1620+ expected_dt = datetime (
1621+ 2023 ,
1622+ 12 ,
1623+ 25 ,
1624+ 15 ,
1625+ 30 ,
1626+ 45 ,
1627+ 123456 , # microseconds = nanoseconds // 1000
1628+ timezone (timedelta (hours = 5 , minutes = 30 )),
1629+ )
1630+ assert result == expected_dt
1631+ assert result .tzinfo == timezone (timedelta (hours = 5 , minutes = 30 ))
1632+
1633+
1634+ def test_mssql_pyodbc_connection_negative_timezone_offset ():
1635+ """Test DATETIMEOFFSET handling with negative timezone offset at connection level."""
1636+ from datetime import datetime , timezone , timedelta
1637+ import struct
1638+ from unittest .mock import Mock , patch
1639+
1640+ with patch ("pyodbc.connect" ) as mock_pyodbc_connect :
1641+ converter_calls = []
1642+
1643+ def mock_add_output_converter (sql_type , converter_func ):
1644+ converter_calls .append ((sql_type , converter_func ))
1645+
1646+ mock_connection = Mock ()
1647+ mock_connection .add_output_converter = mock_add_output_converter
1648+ mock_pyodbc_connect .return_value = mock_connection
1649+
1650+ config = MSSQLConnectionConfig (
1651+ host = "localhost" ,
1652+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1653+ check_import = False ,
1654+ )
1655+
1656+ factory_with_kwargs = config ._connection_factory_with_kwargs
1657+ connection = factory_with_kwargs ()
1658+
1659+ # Get the converter function
1660+ _ , converter_func = converter_calls [0 ]
1661+
1662+ # Test with negative timezone offset: 2023-01-01 12:00:00.0 -08:00
1663+ year , month , day = 2023 , 1 , 1
1664+ hour , minute , second = 12 , 0 , 0
1665+ nanoseconds = 0
1666+ tz_hour_offset , tz_minute_offset = - 8 , 0
1667+
1668+ binary_data = struct .pack (
1669+ "<6hI2h" ,
1670+ year ,
1671+ month ,
1672+ day ,
1673+ hour ,
1674+ minute ,
1675+ second ,
1676+ nanoseconds ,
1677+ tz_hour_offset ,
1678+ tz_minute_offset ,
1679+ )
1680+
1681+ result = converter_func (binary_data )
1682+
1683+ expected_dt = datetime (2023 , 1 , 1 , 12 , 0 , 0 , 0 , timezone (timedelta (hours = - 8 , minutes = 0 )))
1684+ assert result == expected_dt
1685+ assert result .tzinfo == timezone (timedelta (hours = - 8 ))
0 commit comments