Skip to content

Commit 9bc78ae

Browse files
authored
FIX: Stored datetime.time values have the microseconds attribute set to zero (#479)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below For external contributors: Insert Github Issue number below Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#38820](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/38820) <!-- External contributors: GitHub Issue --> > GitHub Issue: #203 ------------------------------------------------------------------- ### Summary This pull request introduces significant improvements to how SQL TIME/TIME2 values are handled in the MSSQL Python driver, transitioning from native C-type bindings to text-based representations. The changes ensure correct parsing, binding, and conversion between SQL TIME values and Python `datetime.time` objects, addressing edge cases and improving compatibility. ### SQL TIME/TIME2 Handling Improvements * Changed TIME/TIME2 parameter binding in `mssql_python/cursor.py` to use `SQL_TYPE_TIME` and text C-types (`SQL_C_CHAR`), and normalized Python `datetime.time` values to ISO text format with microseconds. * Updated C++ bindings in `ddbc_bindings.cpp` to bind and fetch TIME/TIME2 columns as text buffers instead of native structs, and introduced a robust parser (`ParseSqlTimeTextToPythonObject`) for converting SQL time text to Python objects. * Adjusted row size calculations and buffer allocations for TIME/TIME2 columns to use the new maximum text length constant (`SQL_TIME_TEXT_MAX_LEN`). ### Testing and Utilities * Exposed the time-text parser as a test helper in the Python module, allowing for unit testing of TIME/TIME2 parsing edge cases. ### Miscellaneous * Improved table cleanup in tests by switching to `drop_table_if_exists` for reliability.
1 parent 2398ffd commit 9bc78ae

4 files changed

Lines changed: 266 additions & 37 deletions

File tree

mssql_python/cursor.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@
5151
MONEY_MAX: decimal.Decimal = decimal.Decimal("922337203685477.5807")
5252

5353

54+
def _normalize_time_param(value, c_type):
55+
"""Convert a datetime.time to its isoformat string when bound via text C-types.
56+
57+
Returns the isoformat string if conversion applies, otherwise *None*.
58+
"""
59+
if isinstance(value, datetime.time) and c_type in (
60+
ddbc_sql_const.SQL_C_CHAR.value,
61+
ddbc_sql_const.SQL_C_WCHAR.value,
62+
):
63+
return value.isoformat(timespec="microseconds")
64+
return None
65+
66+
5467
class Cursor: # pylint: disable=too-many-instance-attributes,too-many-public-methods
5568
"""
5669
Represents a database cursor, which is used to manage the context of a fetch operation.
@@ -676,10 +689,10 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg
676689

677690
if isinstance(param, datetime.time):
678691
return (
679-
ddbc_sql_const.SQL_TIME.value,
680-
ddbc_sql_const.SQL_C_TYPE_TIME.value,
681-
8,
682-
0,
692+
ddbc_sql_const.SQL_TYPE_TIME.value,
693+
ddbc_sql_const.SQL_C_CHAR.value,
694+
16,
695+
6,
683696
False,
684697
)
685698

@@ -958,6 +971,13 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many
958971
parameter, parameters_list, i, min_val=min_val, max_val=max_val
959972
)
960973

974+
# If TIME values are being bound via text C-types, normalize them to a
975+
# textual representation expected by SQL_C_CHAR/SQL_C_WCHAR binding.
976+
time_text = _normalize_time_param(parameter, c_type)
977+
if time_text is not None:
978+
parameters_list[i] = time_text
979+
column_size = max(column_size, len(time_text))
980+
961981
paraminfo.paramCType = c_type
962982
paraminfo.paramSQLType = sql_type
963983
paraminfo.inputOutputType = ddbc_sql_const.SQL_PARAM_INPUT.value
@@ -2277,6 +2297,10 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
22772297
for i, val in enumerate(processed_row):
22782298
if val is None:
22792299
continue
2300+
time_text = _normalize_time_param(val, parameters_type[i].paramCType)
2301+
if time_text is not None:
2302+
processed_row[i] = time_text
2303+
continue
22802304
if (
22812305
isinstance(val, decimal.Decimal)
22822306
and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "connection/connection_pool.h"
1010
#include "logger_bridge.hpp"
1111

12+
#include <cctype>
1213
#include <cstdint>
1314
#include <cstring> // For std::memcpy
1415
#include <filesystem>
@@ -23,6 +24,7 @@
2324
// These constants are not exposed via sql.h, hence define them here
2425
#define SQL_SS_TIME2 (-154)
2526
#define SQL_SS_TIMESTAMPOFFSET (-155)
27+
#define SQL_C_SS_TIME2 (0x4000)
2628
#define SQL_C_SS_TIMESTAMPOFFSET (0x4001)
2729
#define MAX_DIGITS_IN_NUMERIC 64
2830
#define SQL_MAX_NUMERIC_LEN 16
@@ -66,6 +68,10 @@ inline std::string GetEffectiveCharDecoding(const std::string& userEncoding) {
6668
#endif
6769
}
6870

71+
namespace PythonObjectCache {
72+
py::object get_time_class();
73+
}
74+
6975
//-------------------------------------------------------------------------------------------------
7076
//-------------------------------------------------------------------------------------------------
7177
// Logging Infrastructure:
@@ -3076,7 +3082,7 @@ SQLSMALLINT MapVariantCTypeToSQLType(SQLLEN variantCType) {
30763082
case SQL_C_TIME:
30773083
case SQL_C_TYPE_TIME:
30783084
case SQL_SS_VARIANT_TIME:
3079-
return SQL_TYPE_TIME;
3085+
return SQL_SS_TIME2;
30803086
case SQL_C_TIMESTAMP:
30813087
case SQL_C_TYPE_TIMESTAMP:
30823088
return SQL_TYPE_TIMESTAMP;
@@ -3481,19 +3487,20 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
34813487
}
34823488
break;
34833489
}
3484-
case SQL_TIME:
34853490
case SQL_TYPE_TIME:
34863491
case SQL_SS_TIME2: {
3487-
SQL_TIME_STRUCT timeValue;
3488-
ret =
3489-
SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIME, &timeValue, sizeof(timeValue), NULL);
3490-
if (SQL_SUCCEEDED(ret)) {
3491-
row.append(PythonObjectCache::get_time_class()(timeValue.hour, timeValue.minute,
3492-
timeValue.second));
3492+
SQL_SS_TIME2_STRUCT t2 = {};
3493+
SQLLEN indicator = 0;
3494+
ret = SQLGetData_ptr(hStmt, i, SQL_C_SS_TIME2, &t2, sizeof(t2), &indicator);
3495+
if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
3496+
row.append(PythonObjectCache::get_time_class()(
3497+
t2.hour, t2.minute, t2.second, t2.fraction / 1000)); // ns to µs
34933498
} else {
3494-
LOG("SQLGetData: Error retrieving SQL_TYPE_TIME for column "
3495-
"%d - SQLRETURN=%d",
3496-
i, ret);
3499+
if (!SQL_SUCCEEDED(ret)) {
3500+
LOG("SQLGetData: Error retrieving SQL_SS_TIME2 for column "
3501+
"%d - SQLRETURN=%d",
3502+
i, ret);
3503+
}
34973504
row.append(py::none());
34983505
}
34993506
break;
@@ -3668,7 +3675,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
36683675
default:
36693676
std::ostringstream errorString;
36703677
errorString << "Unsupported data type for column - " << columnName << ", Type - "
3671-
<< dataType << ", column ID - " << i;
3678+
<< effectiveDataType << ", column ID - " << i;
36723679
LOG("SQLGetData: %s", errorString.str().c_str());
36733680
ThrowStdException(errorString.str());
36743681
break;
@@ -3822,13 +3829,11 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column
38223829
SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_DATE, buffers.dateBuffers[col - 1].data(),
38233830
sizeof(SQL_DATE_STRUCT), buffers.indicators[col - 1].data());
38243831
break;
3825-
case SQL_TIME:
3826-
case SQL_TYPE_TIME:
38273832
case SQL_SS_TIME2:
38283833
buffers.timeBuffers[col - 1].resize(fetchSize);
38293834
ret =
3830-
SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_TIME, buffers.timeBuffers[col - 1].data(),
3831-
sizeof(SQL_TIME_STRUCT), buffers.indicators[col - 1].data());
3835+
SQLBindCol_ptr(hStmt, col, SQL_C_SS_TIME2, buffers.timeBuffers[col - 1].data(),
3836+
sizeof(SQL_SS_TIME2_STRUCT), buffers.indicators[col - 1].data());
38323837
break;
38333838
case SQL_GUID:
38343839
buffers.guidBuffers[col - 1].resize(fetchSize);
@@ -4132,13 +4137,11 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
41324137
PyList_SET_ITEM(row, col - 1, dateObj);
41334138
break;
41344139
}
4135-
case SQL_TIME:
4136-
case SQL_TYPE_TIME:
41374140
case SQL_SS_TIME2: {
4141+
const SQL_SS_TIME2_STRUCT& t2 = buffers.timeBuffers[col - 1][i];
41384142
PyObject* timeObj =
4139-
PythonObjectCache::get_time_class()(buffers.timeBuffers[col - 1][i].hour,
4140-
buffers.timeBuffers[col - 1][i].minute,
4141-
buffers.timeBuffers[col - 1][i].second)
4143+
PythonObjectCache::get_time_class()(t2.hour, t2.minute, t2.second,
4144+
t2.fraction / 1000) // ns to µs
41424145
.release()
41434146
.ptr();
41444147
PyList_SET_ITEM(row, col - 1, timeObj);
@@ -4271,10 +4274,8 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) {
42714274
case SQL_TYPE_DATE:
42724275
rowSize += sizeof(SQL_DATE_STRUCT);
42734276
break;
4274-
case SQL_TIME:
4275-
case SQL_TYPE_TIME:
42764277
case SQL_SS_TIME2:
4277-
rowSize += sizeof(SQL_TIME_STRUCT);
4278+
rowSize += sizeof(SQL_SS_TIME2_STRUCT);
42784279
break;
42794280
case SQL_GUID:
42804281
rowSize += sizeof(SQLGUID);
@@ -4969,9 +4970,9 @@ SQLRETURN FetchArrowBatch_wrap(
49694970
case SQL_SS_TIME2: {
49704971
buffers.timeBuffers[idxCol].resize(1);
49714972
ret = SQLGetData_ptr(
4972-
hStmt, idxCol + 1, SQL_C_TYPE_TIME,
4973+
hStmt, idxCol + 1, SQL_C_SS_TIME2,
49734974
buffers.timeBuffers[idxCol].data(),
4974-
sizeof(SQL_TIME_STRUCT),
4975+
sizeof(SQL_SS_TIME2_STRUCT),
49754976
buffers.indicators[idxCol].data()
49764977
);
49774978
if (!SQL_SUCCEEDED(ret)) {
@@ -5228,9 +5229,7 @@ SQLRETURN FetchArrowBatch_wrap(
52285229
case SQL_TIME:
52295230
case SQL_TYPE_TIME:
52305231
case SQL_SS_TIME2: {
5231-
// NOTE: SQL_SS_TIME2 supports fractional seconds, but SQL_C_TYPE_TIME does not.
5232-
// To fully support SQL_SS_TIME2, the corresponding c-type should be used.
5233-
const SQL_TIME_STRUCT& timeValue = buffers.timeBuffers[idxCol][idxRowSql];
5232+
const SQL_SS_TIME2_STRUCT& timeValue = buffers.timeBuffers[idxCol][idxRowSql];
52345233
arrowColumnProducer->timeSecondVal[idxRowArrow] =
52355234
static_cast<int32_t>(timeValue.hour) * 3600 +
52365235
static_cast<int32_t>(timeValue.minute) * 60 +
@@ -5713,6 +5712,8 @@ PYBIND11_MODULE(ddbc_bindings, m) {
57135712
// Expose architecture-specific constants
57145713
m.attr("ARCHITECTURE") = ARCHITECTURE;
57155714

5715+
m.attr("SQL_NO_TOTAL") = static_cast<int>(SQL_NO_TOTAL);
5716+
57165717
// Expose the C++ functions to Python
57175718
m.def("ThrowStdException", &ThrowStdException);
57185719
m.def("GetDriverPathCpp", &GetDriverPathCpp, "Get the path to the ODBC driver");

mssql_python/pybind/ddbc_bindings.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,14 @@ void DDBCSetDecimalSeparator(const std::string& separator);
618618
// (Used internally by ddbc_bindings.cpp - not part of public API)
619619
//-------------------------------------------------------------------------------------------------
620620

621+
// Struct to hold the SQL Server TIME2 structure (SQL_C_SS_TIME2)
622+
struct SQL_SS_TIME2_STRUCT {
623+
SQLUSMALLINT hour;
624+
SQLUSMALLINT minute;
625+
SQLUSMALLINT second;
626+
SQLUINTEGER fraction; // Nanoseconds
627+
};
628+
621629
// Struct to hold the DateTimeOffset structure
622630
struct DateTimeOffset {
623631
SQLSMALLINT year;
@@ -642,7 +650,7 @@ struct ColumnBuffers {
642650
std::vector<std::vector<SQL_TIMESTAMP_STRUCT>> timestampBuffers;
643651
std::vector<std::vector<SQLBIGINT>> bigIntBuffers;
644652
std::vector<std::vector<SQL_DATE_STRUCT>> dateBuffers;
645-
std::vector<std::vector<SQL_TIME_STRUCT>> timeBuffers;
653+
std::vector<std::vector<SQL_SS_TIME2_STRUCT>> timeBuffers;
646654
std::vector<std::vector<SQLGUID>> guidBuffers;
647655
std::vector<std::vector<SQLLEN>> indicators;
648656
std::vector<std::vector<DateTimeOffset>> datetimeoffsetBuffers;

0 commit comments

Comments
 (0)