@@ -1557,3 +1557,184 @@ 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_cursor_init_datetimeoffset_handling ():
1563+ """Test that the MSSQL cursor init properly handles DATETIMEOFFSET conversion."""
1564+ from datetime import datetime , timezone , timedelta
1565+ import struct
1566+ from unittest .mock import Mock
1567+
1568+ config = MSSQLConnectionConfig (
1569+ host = "localhost" ,
1570+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1571+ check_import = False ,
1572+ )
1573+
1574+ # Get the cursor init function
1575+ cursor_init = config ._cursor_init
1576+ assert cursor_init is not None
1577+
1578+ # Create a mock cursor and connection
1579+ mock_connection = Mock ()
1580+ mock_cursor = Mock ()
1581+ mock_cursor .connection = mock_connection
1582+
1583+ # Track calls to add_output_converter
1584+ converter_calls = []
1585+
1586+ def mock_add_output_converter (sql_type , converter_func ):
1587+ converter_calls .append ((sql_type , converter_func ))
1588+
1589+ mock_connection .add_output_converter = mock_add_output_converter
1590+
1591+ # Call the cursor init function
1592+ cursor_init (mock_cursor )
1593+
1594+ # Verify that add_output_converter was called for SQL type -155 (DATETIMEOFFSET)
1595+ assert len (converter_calls ) == 1
1596+ sql_type , converter_func = converter_calls [0 ]
1597+ assert sql_type == - 155
1598+
1599+ # Test the converter function with actual DATETIMEOFFSET binary data
1600+ # Create a test DATETIMEOFFSET value: 2023-12-25 15:30:45.123456789 +05:30
1601+ year , month , day = 2023 , 12 , 25
1602+ hour , minute , second = 15 , 30 , 45
1603+ nanoseconds = 123456789
1604+ tz_hour_offset , tz_minute_offset = 5 , 30
1605+
1606+ # Pack the binary data according to the DATETIMEOFFSET format
1607+ binary_data = struct .pack (
1608+ "<6hI2h" ,
1609+ year ,
1610+ month ,
1611+ day ,
1612+ hour ,
1613+ minute ,
1614+ second ,
1615+ nanoseconds ,
1616+ tz_hour_offset ,
1617+ tz_minute_offset ,
1618+ )
1619+
1620+ # Convert using the registered converter
1621+ result = converter_func (binary_data )
1622+
1623+ # Verify the result
1624+ expected_dt = datetime (
1625+ 2023 ,
1626+ 12 ,
1627+ 25 ,
1628+ 15 ,
1629+ 30 ,
1630+ 45 ,
1631+ 123456 , # microseconds = nanoseconds // 1000
1632+ timezone (timedelta (hours = 5 , minutes = 30 )),
1633+ )
1634+ assert result == expected_dt
1635+ assert result .tzinfo == timezone (timedelta (hours = 5 , minutes = 30 ))
1636+
1637+
1638+ def test_mssql_cursor_init_negative_timezone_offset ():
1639+ """Test DATETIMEOFFSET handling with negative timezone offset."""
1640+ from datetime import datetime , timezone , timedelta
1641+ import struct
1642+ from unittest .mock import Mock
1643+
1644+ config = MSSQLConnectionConfig (
1645+ host = "localhost" ,
1646+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1647+ check_import = False ,
1648+ )
1649+
1650+ cursor_init = config ._cursor_init
1651+ mock_connection = Mock ()
1652+ mock_cursor = Mock ()
1653+ mock_cursor .connection = mock_connection
1654+
1655+ converter_calls = []
1656+
1657+ def mock_add_output_converter (sql_type , converter_func ):
1658+ converter_calls .append ((sql_type , converter_func ))
1659+
1660+ mock_connection .add_output_converter = mock_add_output_converter
1661+ cursor_init (mock_cursor )
1662+
1663+ # Get the converter function
1664+ _ , converter_func = converter_calls [0 ]
1665+
1666+ # Test with negative timezone offset: 2023-01-01 12:00:00.0 -08:00
1667+ year , month , day = 2023 , 1 , 1
1668+ hour , minute , second = 12 , 0 , 0
1669+ nanoseconds = 0
1670+ tz_hour_offset , tz_minute_offset = - 8 , 0
1671+
1672+ binary_data = struct .pack (
1673+ "<6hI2h" ,
1674+ year ,
1675+ month ,
1676+ day ,
1677+ hour ,
1678+ minute ,
1679+ second ,
1680+ nanoseconds ,
1681+ tz_hour_offset ,
1682+ tz_minute_offset ,
1683+ )
1684+
1685+ result = converter_func (binary_data )
1686+
1687+ expected_dt = datetime (2023 , 1 , 1 , 12 , 0 , 0 , 0 , timezone (timedelta (hours = - 8 , minutes = 0 )))
1688+ assert result == expected_dt
1689+ assert result .tzinfo == timezone (timedelta (hours = - 8 ))
1690+
1691+
1692+ def test_mssql_cursor_init_no_add_output_converter ():
1693+ """Test that cursor init gracefully handles connections without add_output_converter."""
1694+ from unittest .mock import Mock
1695+
1696+ config = MSSQLConnectionConfig (
1697+ host = "localhost" ,
1698+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1699+ check_import = False ,
1700+ )
1701+
1702+ cursor_init = config ._cursor_init
1703+ assert cursor_init is not None
1704+
1705+ # Create a mock cursor and connection without add_output_converter
1706+ mock_connection = Mock ()
1707+ mock_cursor = Mock ()
1708+ mock_cursor .connection = mock_connection
1709+
1710+ # Remove the add_output_converter attribute
1711+ if hasattr (mock_connection , "add_output_converter" ):
1712+ delattr (mock_connection , "add_output_converter" )
1713+
1714+ # This should not raise an exception
1715+ cursor_init (mock_cursor )
1716+
1717+
1718+ def test_mssql_cursor_init_returns_callable_for_pyodbc ():
1719+ """Test that _cursor_init returns a callable function for pyodbc driver."""
1720+ config = MSSQLConnectionConfig (
1721+ host = "localhost" ,
1722+ driver = "pyodbc" ,
1723+ check_import = False ,
1724+ )
1725+
1726+ cursor_init = config ._cursor_init
1727+ assert cursor_init is not None
1728+ assert callable (cursor_init )
1729+
1730+
1731+ def test_mssql_cursor_init_returns_none_for_pymssql ():
1732+ """Test that _cursor_init returns None for pymssql driver."""
1733+ config = MSSQLConnectionConfig (
1734+ host = "localhost" ,
1735+ driver = "pymssql" ,
1736+ check_import = False ,
1737+ )
1738+
1739+ cursor_init = config ._cursor_init
1740+ assert cursor_init is None
0 commit comments