Skip to content

Commit 4142a97

Browse files
committed
FIX: Stored datetime.time values have the microseconds attribute set to zero #203
1 parent 064f543 commit 4142a97

3 files changed

Lines changed: 772 additions & 324 deletions

File tree

mssql_python/cursor.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -673,10 +673,10 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
673673

674674
if isinstance(param, datetime.time):
675675
return (
676-
ddbc_sql_const.SQL_TIME.value,
677-
ddbc_sql_const.SQL_C_TYPE_TIME.value,
678-
8,
679-
0,
676+
ddbc_sql_const.SQL_TYPE_TIME.value,
677+
ddbc_sql_const.SQL_C_CHAR.value,
678+
16,
679+
6,
680680
False,
681681
)
682682

@@ -941,6 +941,16 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many
941941
parameter, parameters_list, i, min_val=min_val, max_val=max_val
942942
)
943943

944+
# If TIME values are being bound via text C-types, normalize them to a
945+
# textual representation expected by SQL_C_CHAR/SQL_C_WCHAR binding.
946+
if isinstance(parameter, datetime.time) and c_type in (
947+
ddbc_sql_const.SQL_C_CHAR.value,
948+
ddbc_sql_const.SQL_C_WCHAR.value,
949+
):
950+
time_text = parameter.isoformat(timespec="microseconds")
951+
parameters_list[i] = time_text
952+
column_size = max(column_size, len(time_text))
953+
944954
paraminfo.paramCType = c_type
945955
paraminfo.paramSQLType = sql_type
946956
paraminfo.inputOutputType = ddbc_sql_const.SQL_PARAM_INPUT.value
@@ -2250,6 +2260,12 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
22502260
for i, val in enumerate(processed_row):
22512261
if val is None:
22522262
continue
2263+
if isinstance(val, datetime.time) and parameters_type[i].paramCType in (
2264+
ddbc_sql_const.SQL_C_CHAR.value,
2265+
ddbc_sql_const.SQL_C_WCHAR.value,
2266+
):
2267+
processed_row[i] = val.isoformat(timespec="microseconds")
2268+
continue
22532269
if (
22542270
isinstance(val, decimal.Decimal)
22552271
and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "logger_bridge.hpp"
1111

1212
#include <cstdint>
13+
#include <cctype>
1314
#include <cstring> // For std::memcpy
1415
#include <filesystem>
1516
#include <iomanip> // std::setw, std::setfill
@@ -28,6 +29,7 @@
2829
#define SQL_MAX_NUMERIC_LEN 16
2930
#define SQL_SS_XML (-152)
3031
#define SQL_SS_UDT (-151)
32+
#define SQL_TIME_TEXT_MAX_LEN 32
3133

3234
#define STRINGIFY_FOR_CASE(x) \
3335
case x: \
@@ -53,6 +55,69 @@ inline std::string GetEffectiveCharDecoding(const std::string& userEncoding) {
5355
#endif
5456
}
5557

58+
namespace PythonObjectCache {
59+
py::object get_time_class();
60+
}
61+
62+
inline py::object ParseSqlTimeTextToPythonObject(const char* timeText, SQLLEN timeTextLen) {
63+
if (!timeText || timeTextLen <= 0) {
64+
return py::none();
65+
}
66+
67+
size_t len = static_cast<size_t>(timeTextLen);
68+
if (timeTextLen == SQL_NO_TOTAL) {
69+
len = std::strlen(timeText);
70+
}
71+
72+
std::string value(timeText, len);
73+
74+
size_t start = value.find_first_not_of(" \t\r\n");
75+
if (start == std::string::npos) {
76+
return py::none();
77+
}
78+
size_t end = value.find_last_not_of(" \t\r\n");
79+
value = value.substr(start, end - start + 1);
80+
81+
size_t firstColon = value.find(':');
82+
size_t secondColon = (firstColon == std::string::npos) ? std::string::npos
83+
: value.find(':', firstColon + 1);
84+
if (firstColon == std::string::npos || secondColon == std::string::npos) {
85+
ThrowStdException("Failed to parse TIME/TIME2 value: missing ':' separators");
86+
}
87+
88+
int hour = std::stoi(value.substr(0, firstColon));
89+
int minute = std::stoi(value.substr(firstColon + 1, secondColon - firstColon - 1));
90+
91+
size_t dotPos = value.find('.', secondColon + 1);
92+
int second = 0;
93+
int microsecond = 0;
94+
95+
if (dotPos == std::string::npos) {
96+
second = std::stoi(value.substr(secondColon + 1));
97+
} else {
98+
second = std::stoi(value.substr(secondColon + 1, dotPos - secondColon - 1));
99+
std::string frac = value.substr(dotPos + 1);
100+
101+
size_t digitCount = 0;
102+
while (digitCount < frac.size() && std::isdigit(static_cast<unsigned char>(frac[digitCount]))) {
103+
++digitCount;
104+
}
105+
frac = frac.substr(0, digitCount);
106+
107+
if (frac.size() > 6) {
108+
frac = frac.substr(0, 6);
109+
}
110+
while (frac.size() < 6) {
111+
frac.push_back('0');
112+
}
113+
if (!frac.empty()) {
114+
microsecond = std::stoi(frac);
115+
}
116+
}
117+
118+
return PythonObjectCache::get_time_class()(hour, minute, second, microsecond);
119+
}
120+
56121
//-------------------------------------------------------------------------------------------------
57122
//-------------------------------------------------------------------------------------------------
58123
// Logging Infrastructure:
@@ -3244,9 +3309,23 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
32443309
}
32453310
break;
32463311
}
3247-
case SQL_TIME:
3248-
case SQL_TYPE_TIME:
32493312
case SQL_SS_TIME2: {
3313+
char timeTextBuffer[SQL_TIME_TEXT_MAX_LEN] = {0};
3314+
SQLLEN timeDataLen = 0;
3315+
ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, &timeTextBuffer, sizeof(timeTextBuffer),
3316+
&timeDataLen);
3317+
if (SQL_SUCCEEDED(ret) && timeDataLen != SQL_NULL_DATA) {
3318+
row.append(ParseSqlTimeTextToPythonObject(timeTextBuffer, timeDataLen));
3319+
} else {
3320+
LOG("SQLGetData: Error retrieving SQL_SS_TIME2 for column "
3321+
"%d - SQLRETURN=%d",
3322+
i, ret);
3323+
row.append(py::none());
3324+
}
3325+
break;
3326+
}
3327+
case SQL_TIME:
3328+
case SQL_TYPE_TIME: {
32503329
SQL_TIME_STRUCT timeValue;
32513330
ret =
32523331
SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIME, &timeValue, sizeof(timeValue), NULL);
@@ -3587,12 +3666,16 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column
35873666
break;
35883667
case SQL_TIME:
35893668
case SQL_TYPE_TIME:
3590-
case SQL_SS_TIME2:
35913669
buffers.timeBuffers[col - 1].resize(fetchSize);
35923670
ret =
35933671
SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_TIME, buffers.timeBuffers[col - 1].data(),
35943672
sizeof(SQL_TIME_STRUCT), buffers.indicators[col - 1].data());
35953673
break;
3674+
case SQL_SS_TIME2:
3675+
buffers.charBuffers[col - 1].resize(fetchSize * SQL_TIME_TEXT_MAX_LEN);
3676+
ret = SQLBindCol_ptr(hStmt, col, SQL_C_CHAR, buffers.charBuffers[col - 1].data(),
3677+
SQL_TIME_TEXT_MAX_LEN, buffers.indicators[col - 1].data());
3678+
break;
35963679
case SQL_GUID:
35973680
buffers.guidBuffers[col - 1].resize(fetchSize);
35983681
ret = SQLBindCol_ptr(hStmt, col, SQL_C_GUID, buffers.guidBuffers[col - 1].data(),
@@ -3896,8 +3979,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
38963979
break;
38973980
}
38983981
case SQL_TIME:
3899-
case SQL_TYPE_TIME:
3900-
case SQL_SS_TIME2: {
3982+
case SQL_TYPE_TIME: {
39013983
PyObject* timeObj =
39023984
PythonObjectCache::get_time_class()(buffers.timeBuffers[col - 1][i].hour,
39033985
buffers.timeBuffers[col - 1][i].minute,
@@ -3907,6 +3989,14 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
39073989
PyList_SET_ITEM(row, col - 1, timeObj);
39083990
break;
39093991
}
3992+
case SQL_SS_TIME2: {
3993+
const char* rawData = reinterpret_cast<const char*>(
3994+
&buffers.charBuffers[col - 1][i * SQL_TIME_TEXT_MAX_LEN]);
3995+
SQLLEN timeDataLen = buffers.indicators[col - 1][i];
3996+
py::object timeObj = ParseSqlTimeTextToPythonObject(rawData, timeDataLen);
3997+
PyList_SET_ITEM(row, col - 1, timeObj.release().ptr());
3998+
break;
3999+
}
39104000
case SQL_SS_TIMESTAMPOFFSET: {
39114001
SQLULEN rowIdx = i;
39124002
const DateTimeOffset& dtoValue = buffers.datetimeoffsetBuffers[col - 1][rowIdx];
@@ -4036,9 +4126,11 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) {
40364126
break;
40374127
case SQL_TIME:
40384128
case SQL_TYPE_TIME:
4039-
case SQL_SS_TIME2:
40404129
rowSize += sizeof(SQL_TIME_STRUCT);
40414130
break;
4131+
case SQL_SS_TIME2:
4132+
rowSize += SQL_TIME_TEXT_MAX_LEN;
4133+
break;
40424134
case SQL_GUID:
40434135
rowSize += sizeof(SQLGUID);
40444136
break;

0 commit comments

Comments
 (0)