Skip to content

Commit 36d5294

Browse files
committed
fix(sqlserver): handle datetime2 7-digit fractional strings in adapters
ODBC and older Python runtimes deliver datetime2 values as strings with seven fractional digits (100ns precision). datetime.fromisoformat capped at 6 digits on Python < 3.13, raising ValueError during row adaption. Normalize the string before parsing: map 'Z' to +00:00, restore the T separator, and drop fractional digits beyond 6.
1 parent 3cf2855 commit 36d5294

1 file changed

Lines changed: 28 additions & 5 deletions

File tree

  • src/rhosocial/activerecord/backend/impl/sqlserver

‎src/rhosocial/activerecord/backend/impl/sqlserver/adapters.py‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,32 @@
1313
from typing import Any, Dict, List, Type, Union
1414

1515

16+
def _parse_sql_datetime(value: str) -> datetime:
17+
"""Parse a SQL Server datetime string into a timezone-aware datetime.
18+
19+
ODBC/older Python runtimes deliver datetime2 values as strings without a
20+
``T`` separator and often carry seven fractional digits (100ns precision),
21+
e.g. ``'2026-08-09 05:25:26.5205700'``. Python's ``datetime.fromisoformat``
22+
rejects values with more than six fractional digits on Python < 3.13, so
23+
normalize the string first: normalize the separator, then drop any
24+
fractional component beyond six digits.
25+
"""
26+
normalized = value.replace('Z', '+00:00')
27+
if 'T' not in normalized:
28+
head, sep, rest = normalized.partition(' ')
29+
if sep:
30+
normalized = f"{head}T{rest}"
31+
if '.' in normalized:
32+
head, sep, frac = normalized.rpartition('.')
33+
frac = frac.rstrip('0')[:6]
34+
if frac:
35+
normalized = f"{head}.{frac}"
36+
result = datetime.fromisoformat(normalized)
37+
if result.tzinfo is None:
38+
return result.replace(tzinfo=timezone.utc)
39+
return result
40+
41+
1642
class TypeAdapter(ABC):
1743
"""Base class for type adapters."""
1844

@@ -114,10 +140,7 @@ def from_database(self, value: Any, target_type: Type = None, options: Dict = No
114140
return value
115141

116142
if isinstance(value, str):
117-
result = datetime.fromisoformat(value.replace('Z', '+00:00'))
118-
if result.tzinfo is None:
119-
return result.replace(tzinfo=timezone.utc)
120-
return result
143+
return _parse_sql_datetime(value)
121144

122145
raise TypeError(f"Cannot convert {type(value)} to datetime")
123146

@@ -155,7 +178,7 @@ def from_database(self, value: Any, target_type: Type = None, options: Dict = No
155178
return value
156179

157180
if isinstance(value, str):
158-
return datetime.fromisoformat(value)
181+
return _parse_sql_datetime(value)
159182

160183
raise TypeError(f"Cannot convert {type(value)} to datetime")
161184

0 commit comments

Comments
 (0)