diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 74868f94..605120bf 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -54,6 +54,11 @@ # encoding and must be rejected at detect time on both paths (see _map_sql_type). BIGINT_MIN: int = -(2**63) BIGINT_MAX: int = 2**63 - 1 +ODBC3_TEMPORAL_SQL_TYPES = { + ddbc_sql_const.SQL_DATE.value: ddbc_sql_const.SQL_TYPE_DATE.value, + ddbc_sql_const.SQL_TIME.value: ddbc_sql_const.SQL_TYPE_TIME.value, + ddbc_sql_const.SQL_TIMESTAMP.value: ddbc_sql_const.SQL_TYPE_TIMESTAMP.value, +} def _normalize_time_param(value, c_type): @@ -947,7 +952,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg ) # Naive datetime -> TIMESTAMP return ( - ddbc_sql_const.SQL_TIMESTAMP.value, + ddbc_sql_const.SQL_TYPE_TIMESTAMP.value, ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 26, 6, @@ -956,7 +961,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg if isinstance(param, datetime.date): return ( - ddbc_sql_const.SQL_DATE.value, + ddbc_sql_const.SQL_TYPE_DATE.value, ddbc_sql_const.SQL_C_TYPE_DATE.value, 10, 0, @@ -1180,6 +1185,8 @@ def setinputsizes(self, sizes: List[Union[int, tuple]]) -> None: f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant." ) + sql_type = ODBC3_TEMPORAL_SQL_TYPES.get(sql_type, sql_type) + # Validate size and precision if not isinstance(column_size, int) or column_size < 0: raise ValueError( @@ -1210,6 +1217,8 @@ def setinputsizes(self, sizes: List[Union[int, tuple]]) -> None: f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant." ) + sql_type = ODBC3_TEMPORAL_SQL_TYPES.get(sql_type, sql_type) + self._inputsizes.append( (sql_type, self._get_c_type_for_sql_type(sql_type), 0, 0) ) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index a3ddaaa1..219d4833 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2520,6 +2520,58 @@ def test_map_sql_type_none_returns_sql_unknown_type(): assert is_dae is False +def test_map_sql_type_uses_odbc3_temporal_types(): + """Python-side inference uses ODBC 3.x temporal SQL types.""" + from unittest.mock import MagicMock + + from mssql_python.constants import ConstantsDDBC as ddbc_sql_const + + cursor = MagicMock(spec=mssql_python.Cursor) + _map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor) + + date_type = _map_sql_type(date(2025, 1, 1), [date(2025, 1, 1)], 0) + datetime_type = _map_sql_type(datetime(2025, 1, 1), [datetime(2025, 1, 1)], 0) + + assert date_type[0] == ddbc_sql_const.SQL_TYPE_DATE.value + assert datetime_type[0] == ddbc_sql_const.SQL_TYPE_TIMESTAMP.value + + +def test_setinputsizes_canonicalizes_odbc2_temporal_types(): + """Legacy temporal hints are converted before reaching SQLBindParameter.""" + from mssql_python.constants import ConstantsDDBC as ddbc_sql_const + + cursor = object.__new__(mssql_python.Cursor) + + cursor.setinputsizes( + [ + (ddbc_sql_const.SQL_DATE.value, 10, 0), + ddbc_sql_const.SQL_TIME.value, + (ddbc_sql_const.SQL_TIMESTAMP.value, 26, 6), + ] + ) + + assert cursor._inputsizes == [ + ( + ddbc_sql_const.SQL_TYPE_DATE.value, + ddbc_sql_const.SQL_C_TYPE_DATE.value, + 10, + 0, + ), + ( + ddbc_sql_const.SQL_TYPE_TIME.value, + ddbc_sql_const.SQL_C_TYPE_TIME.value, + 0, + 0, + ), + ( + ddbc_sql_const.SQL_TYPE_TIMESTAMP.value, + ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, + 26, + 6, + ), + ] + + # --------------------------------------------------------- # GH-610: SQLDescribeParam cache coverage tests # --------------------------------------------------------- diff --git a/tests/test_023_execute_path_parity.py b/tests/test_023_execute_path_parity.py index 52a3a926..d9bada32 100644 --- a/tests/test_023_execute_path_parity.py +++ b/tests/test_023_execute_path_parity.py @@ -664,10 +664,10 @@ def _param_basetype(cursor, value): (b"", _c.SQL_VARBINARY, _c.SQL_C_BINARY, 1, 0, False), (b"abc", _c.SQL_VARBINARY, _c.SQL_C_BINARY, 3, 0, False), # date / datetime / time - (datetime.date(2024, 1, 1), _c.SQL_DATE, _c.SQL_C_TYPE_DATE, 10, 0, False), + (datetime.date(2024, 1, 1), _c.SQL_TYPE_DATE, _c.SQL_C_TYPE_DATE, 10, 0, False), ( datetime.datetime(2024, 1, 1, 2, 3, 4), - _c.SQL_TIMESTAMP, + _c.SQL_TYPE_TIMESTAMP, _c.SQL_C_TYPE_TIMESTAMP, 26, 6,