Skip to content

Commit cb0bd6c

Browse files
TheekshnaTheekshna Kotian
andauthored
FIX: Use ODBC 3.x parameter types instead of ODBC 2.x (#758)
### Work Item / Issue Reference > Closes #757 ------------------------------------------------------------------- ### Summary Use ODBC 3.x concise temporal SQL types for Python-side parameter inference. Canonicalize legacy temporal hints passed to `setinputsizes()` before binding while preserving exported constant values. Add focused coverage for inferred and explicit temporal types. ### Validation - `python -m black --check --line-length=100 mssql_python/ tests/` - Focused mapping tests: 3 passed - `tests/test_000_dependencies.py`: 35 passed, 3 skipped - Live SQL tests not run locally because `DB_CONNECTION_STRING` is unavailable. --------- Co-authored-by: Theekshna Kotian <tkotian@microsoft.com>
1 parent 331984b commit cb0bd6c

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

‎mssql_python/cursor.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
# encoding and must be rejected at detect time on both paths (see _map_sql_type).
5555
BIGINT_MIN: int = -(2**63)
5656
BIGINT_MAX: int = 2**63 - 1
57+
ODBC3_TEMPORAL_SQL_TYPES = {
58+
ddbc_sql_const.SQL_DATE.value: ddbc_sql_const.SQL_TYPE_DATE.value,
59+
ddbc_sql_const.SQL_TIME.value: ddbc_sql_const.SQL_TYPE_TIME.value,
60+
ddbc_sql_const.SQL_TIMESTAMP.value: ddbc_sql_const.SQL_TYPE_TIMESTAMP.value,
61+
}
5762

5863

5964
def _normalize_time_param(value, c_type):
@@ -947,7 +952,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
947952
)
948953
# Naive datetime -> TIMESTAMP
949954
return (
950-
ddbc_sql_const.SQL_TIMESTAMP.value,
955+
ddbc_sql_const.SQL_TYPE_TIMESTAMP.value,
951956
ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
952957
26,
953958
6,
@@ -956,7 +961,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
956961

957962
if isinstance(param, datetime.date):
958963
return (
959-
ddbc_sql_const.SQL_DATE.value,
964+
ddbc_sql_const.SQL_TYPE_DATE.value,
960965
ddbc_sql_const.SQL_C_TYPE_DATE.value,
961966
10,
962967
0,
@@ -1180,6 +1185,8 @@ def setinputsizes(self, sizes: List[Union[int, tuple]]) -> None:
11801185
f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant."
11811186
)
11821187

1188+
sql_type = ODBC3_TEMPORAL_SQL_TYPES.get(sql_type, sql_type)
1189+
11831190
# Validate size and precision
11841191
if not isinstance(column_size, int) or column_size < 0:
11851192
raise ValueError(
@@ -1210,6 +1217,8 @@ def setinputsizes(self, sizes: List[Union[int, tuple]]) -> None:
12101217
f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant."
12111218
)
12121219

1220+
sql_type = ODBC3_TEMPORAL_SQL_TYPES.get(sql_type, sql_type)
1221+
12131222
self._inputsizes.append(
12141223
(sql_type, self._get_c_type_for_sql_type(sql_type), 0, 0)
12151224
)

‎tests/test_004_cursor.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2520,6 +2520,58 @@ def test_map_sql_type_none_returns_sql_unknown_type():
25202520
assert is_dae is False
25212521

25222522

2523+
def test_map_sql_type_uses_odbc3_temporal_types():
2524+
"""Python-side inference uses ODBC 3.x temporal SQL types."""
2525+
from unittest.mock import MagicMock
2526+
2527+
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const
2528+
2529+
cursor = MagicMock(spec=mssql_python.Cursor)
2530+
_map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor)
2531+
2532+
date_type = _map_sql_type(date(2025, 1, 1), [date(2025, 1, 1)], 0)
2533+
datetime_type = _map_sql_type(datetime(2025, 1, 1), [datetime(2025, 1, 1)], 0)
2534+
2535+
assert date_type[0] == ddbc_sql_const.SQL_TYPE_DATE.value
2536+
assert datetime_type[0] == ddbc_sql_const.SQL_TYPE_TIMESTAMP.value
2537+
2538+
2539+
def test_setinputsizes_canonicalizes_odbc2_temporal_types():
2540+
"""Legacy temporal hints are converted before reaching SQLBindParameter."""
2541+
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const
2542+
2543+
cursor = object.__new__(mssql_python.Cursor)
2544+
2545+
cursor.setinputsizes(
2546+
[
2547+
(ddbc_sql_const.SQL_DATE.value, 10, 0),
2548+
ddbc_sql_const.SQL_TIME.value,
2549+
(ddbc_sql_const.SQL_TIMESTAMP.value, 26, 6),
2550+
]
2551+
)
2552+
2553+
assert cursor._inputsizes == [
2554+
(
2555+
ddbc_sql_const.SQL_TYPE_DATE.value,
2556+
ddbc_sql_const.SQL_C_TYPE_DATE.value,
2557+
10,
2558+
0,
2559+
),
2560+
(
2561+
ddbc_sql_const.SQL_TYPE_TIME.value,
2562+
ddbc_sql_const.SQL_C_TYPE_TIME.value,
2563+
0,
2564+
0,
2565+
),
2566+
(
2567+
ddbc_sql_const.SQL_TYPE_TIMESTAMP.value,
2568+
ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
2569+
26,
2570+
6,
2571+
),
2572+
]
2573+
2574+
25232575
# ---------------------------------------------------------
25242576
# GH-610: SQLDescribeParam cache coverage tests
25252577
# ---------------------------------------------------------

‎tests/test_023_execute_path_parity.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,10 +664,10 @@ def _param_basetype(cursor, value):
664664
(b"", _c.SQL_VARBINARY, _c.SQL_C_BINARY, 1, 0, False),
665665
(b"abc", _c.SQL_VARBINARY, _c.SQL_C_BINARY, 3, 0, False),
666666
# date / datetime / time
667-
(datetime.date(2024, 1, 1), _c.SQL_DATE, _c.SQL_C_TYPE_DATE, 10, 0, False),
667+
(datetime.date(2024, 1, 1), _c.SQL_TYPE_DATE, _c.SQL_C_TYPE_DATE, 10, 0, False),
668668
(
669669
datetime.datetime(2024, 1, 1, 2, 3, 4),
670-
_c.SQL_TIMESTAMP,
670+
_c.SQL_TYPE_TIMESTAMP,
671671
_c.SQL_C_TYPE_TIMESTAMP,
672672
26,
673673
6,

0 commit comments

Comments
 (0)