@@ -1521,3 +1521,184 @@ def test_mssql_pymssql_connection_factory():
15211521 # Clean up the mock module
15221522 if "pymssql" in sys .modules :
15231523 del sys .modules ["pymssql" ]
1524+
1525+
1526+ def test_mssql_cursor_init_datetimeoffset_handling ():
1527+ """Test that the MSSQL cursor init properly handles DATETIMEOFFSET conversion."""
1528+ from datetime import datetime , timezone , timedelta
1529+ import struct
1530+ from unittest .mock import Mock
1531+
1532+ config = MSSQLConnectionConfig (
1533+ host = "localhost" ,
1534+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1535+ check_import = False ,
1536+ )
1537+
1538+ # Get the cursor init function
1539+ cursor_init = config ._cursor_init
1540+ assert cursor_init is not None
1541+
1542+ # Create a mock cursor and connection
1543+ mock_connection = Mock ()
1544+ mock_cursor = Mock ()
1545+ mock_cursor .connection = mock_connection
1546+
1547+ # Track calls to add_output_converter
1548+ converter_calls = []
1549+
1550+ def mock_add_output_converter (sql_type , converter_func ):
1551+ converter_calls .append ((sql_type , converter_func ))
1552+
1553+ mock_connection .add_output_converter = mock_add_output_converter
1554+
1555+ # Call the cursor init function
1556+ cursor_init (mock_cursor )
1557+
1558+ # Verify that add_output_converter was called for SQL type -155 (DATETIMEOFFSET)
1559+ assert len (converter_calls ) == 1
1560+ sql_type , converter_func = converter_calls [0 ]
1561+ assert sql_type == - 155
1562+
1563+ # Test the converter function with actual DATETIMEOFFSET binary data
1564+ # Create a test DATETIMEOFFSET value: 2023-12-25 15:30:45.123456789 +05:30
1565+ year , month , day = 2023 , 12 , 25
1566+ hour , minute , second = 15 , 30 , 45
1567+ nanoseconds = 123456789
1568+ tz_hour_offset , tz_minute_offset = 5 , 30
1569+
1570+ # Pack the binary data according to the DATETIMEOFFSET format
1571+ binary_data = struct .pack (
1572+ "<6hI2h" ,
1573+ year ,
1574+ month ,
1575+ day ,
1576+ hour ,
1577+ minute ,
1578+ second ,
1579+ nanoseconds ,
1580+ tz_hour_offset ,
1581+ tz_minute_offset ,
1582+ )
1583+
1584+ # Convert using the registered converter
1585+ result = converter_func (binary_data )
1586+
1587+ # Verify the result
1588+ expected_dt = datetime (
1589+ 2023 ,
1590+ 12 ,
1591+ 25 ,
1592+ 15 ,
1593+ 30 ,
1594+ 45 ,
1595+ 123456 , # microseconds = nanoseconds // 1000
1596+ timezone (timedelta (hours = 5 , minutes = 30 )),
1597+ )
1598+ assert result == expected_dt
1599+ assert result .tzinfo == timezone (timedelta (hours = 5 , minutes = 30 ))
1600+
1601+
1602+ def test_mssql_cursor_init_negative_timezone_offset ():
1603+ """Test DATETIMEOFFSET handling with negative timezone offset."""
1604+ from datetime import datetime , timezone , timedelta
1605+ import struct
1606+ from unittest .mock import Mock
1607+
1608+ config = MSSQLConnectionConfig (
1609+ host = "localhost" ,
1610+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1611+ check_import = False ,
1612+ )
1613+
1614+ cursor_init = config ._cursor_init
1615+ mock_connection = Mock ()
1616+ mock_cursor = Mock ()
1617+ mock_cursor .connection = mock_connection
1618+
1619+ converter_calls = []
1620+
1621+ def mock_add_output_converter (sql_type , converter_func ):
1622+ converter_calls .append ((sql_type , converter_func ))
1623+
1624+ mock_connection .add_output_converter = mock_add_output_converter
1625+ cursor_init (mock_cursor )
1626+
1627+ # Get the converter function
1628+ _ , converter_func = converter_calls [0 ]
1629+
1630+ # Test with negative timezone offset: 2023-01-01 12:00:00.0 -08:00
1631+ year , month , day = 2023 , 1 , 1
1632+ hour , minute , second = 12 , 0 , 0
1633+ nanoseconds = 0
1634+ tz_hour_offset , tz_minute_offset = - 8 , 0
1635+
1636+ binary_data = struct .pack (
1637+ "<6hI2h" ,
1638+ year ,
1639+ month ,
1640+ day ,
1641+ hour ,
1642+ minute ,
1643+ second ,
1644+ nanoseconds ,
1645+ tz_hour_offset ,
1646+ tz_minute_offset ,
1647+ )
1648+
1649+ result = converter_func (binary_data )
1650+
1651+ expected_dt = datetime (2023 , 1 , 1 , 12 , 0 , 0 , 0 , timezone (timedelta (hours = - 8 , minutes = 0 )))
1652+ assert result == expected_dt
1653+ assert result .tzinfo == timezone (timedelta (hours = - 8 ))
1654+
1655+
1656+ def test_mssql_cursor_init_no_add_output_converter ():
1657+ """Test that cursor init gracefully handles connections without add_output_converter."""
1658+ from unittest .mock import Mock
1659+
1660+ config = MSSQLConnectionConfig (
1661+ host = "localhost" ,
1662+ driver = "pyodbc" , # DATETIMEOFFSET handling is pyodbc-specific
1663+ check_import = False ,
1664+ )
1665+
1666+ cursor_init = config ._cursor_init
1667+ assert cursor_init is not None
1668+
1669+ # Create a mock cursor and connection without add_output_converter
1670+ mock_connection = Mock ()
1671+ mock_cursor = Mock ()
1672+ mock_cursor .connection = mock_connection
1673+
1674+ # Remove the add_output_converter attribute
1675+ if hasattr (mock_connection , "add_output_converter" ):
1676+ delattr (mock_connection , "add_output_converter" )
1677+
1678+ # This should not raise an exception
1679+ cursor_init (mock_cursor )
1680+
1681+
1682+ def test_mssql_cursor_init_returns_callable_for_pyodbc ():
1683+ """Test that _cursor_init returns a callable function for pyodbc driver."""
1684+ config = MSSQLConnectionConfig (
1685+ host = "localhost" ,
1686+ driver = "pyodbc" ,
1687+ check_import = False ,
1688+ )
1689+
1690+ cursor_init = config ._cursor_init
1691+ assert cursor_init is not None
1692+ assert callable (cursor_init )
1693+
1694+
1695+ def test_mssql_cursor_init_returns_none_for_pymssql ():
1696+ """Test that _cursor_init returns None for pymssql driver."""
1697+ config = MSSQLConnectionConfig (
1698+ host = "localhost" ,
1699+ driver = "pymssql" ,
1700+ check_import = False ,
1701+ )
1702+
1703+ cursor_init = config ._cursor_init
1704+ assert cursor_init is None
0 commit comments