@@ -1557,3 +1557,129 @@ def test_mssql_pymssql_connection_factory():
1557
1557
# Clean up the mock module
1558
1558
if "pymssql" in sys .modules :
1559
1559
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